8 var UIElement = L.Class.extend({
10 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
11 return this.node.value;
16 setValue: function(value) {
17 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
18 this.node.value = value;
22 return (this.validState !== false);
25 triggerValidation: function() {
26 if (typeof(this.vfunc) != 'function')
29 var wasValid = this.isValid();
33 return (wasValid != this.isValid());
36 registerEvents: function(targetNode, synevent, events) {
37 var dispatchFn = L.bind(function(ev) {
38 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
41 for (var i = 0; i < events.length; i++)
42 targetNode.addEventListener(events[i], dispatchFn);
45 setUpdateEvents: function(targetNode /*, ... */) {
46 var datatype = this.options.datatype,
47 optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
48 validate = this.options.validate,
49 events = this.varargs(arguments, 1);
51 this.registerEvents(targetNode, 'widget-update', events);
53 if (!datatype && !validate)
56 this.vfunc = L.ui.addValidator.apply(L.ui, [
57 targetNode, datatype || 'string',
61 this.node.addEventListener('validation-success', L.bind(function(ev) {
62 this.validState = true;
65 this.node.addEventListener('validation-failure', L.bind(function(ev) {
66 this.validState = false;
70 setChangeEvents: function(targetNode /*, ... */) {
71 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
75 var UITextfield = UIElement.extend({
76 __init__: function(value, options) {
78 this.options = Object.assign({
85 var frameEl = E('div', { 'id': this.options.id });
87 if (this.options.password) {
88 frameEl.classList.add('nowrap');
89 frameEl.appendChild(E('input', {
91 'style': 'position:absolute; left:-100000px',
94 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
98 frameEl.appendChild(E('input', {
99 'name': this.options.name,
100 'type': this.options.password ? 'password' : 'text',
101 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
102 'readonly': this.options.readonly ? '' : null,
103 'maxlength': this.options.maxlength,
104 'placeholder': this.options.placeholder,
108 if (this.options.password)
109 frameEl.appendChild(E('button', {
110 'class': 'cbi-button cbi-button-neutral',
111 'title': _('Reveal/hide password'),
112 'aria-label': _('Reveal/hide password'),
113 'click': function(ev) {
114 var e = this.previousElementSibling;
115 e.type = (e.type === 'password') ? 'text' : 'password';
120 return this.bind(frameEl);
123 bind: function(frameEl) {
124 var inputEl = frameEl.childNodes[+!!this.options.password];
128 this.setUpdateEvents(inputEl, 'keyup', 'blur');
129 this.setChangeEvents(inputEl, 'change');
131 L.dom.bindClassInstance(frameEl, this);
136 getValue: function() {
137 var inputEl = this.node.childNodes[+!!this.options.password];
138 return inputEl.value;
141 setValue: function(value) {
142 var inputEl = this.node.childNodes[+!!this.options.password];
143 inputEl.value = value;
147 var UICheckbox = UIElement.extend({
148 __init__: function(value, options) {
150 this.options = Object.assign({
157 var frameEl = E('div', {
158 'id': this.options.id,
159 'class': 'cbi-checkbox'
162 if (this.options.hiddenname)
163 frameEl.appendChild(E('input', {
165 'name': this.options.hiddenname,
169 frameEl.appendChild(E('input', {
170 'name': this.options.name,
172 'value': this.options.value_enabled,
173 'checked': (this.value == this.options.value_enabled) ? '' : null
176 return this.bind(frameEl);
179 bind: function(frameEl) {
182 this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
183 this.setChangeEvents(frameEl.lastElementChild, 'change');
185 L.dom.bindClassInstance(frameEl, this);
190 isChecked: function() {
191 return this.node.lastElementChild.checked;
194 getValue: function() {
195 return this.isChecked()
196 ? this.options.value_enabled
197 : this.options.value_disabled;
200 setValue: function(value) {
201 this.node.lastElementChild.checked = (value == this.options.value_enabled);
205 var UISelect = UIElement.extend({
206 __init__: function(value, choices, options) {
207 if (typeof(choices) != 'object')
210 if (!Array.isArray(value))
211 value = (value != null && value != '') ? [ value ] : [];
213 if (!options.multi && value.length > 1)
217 this.choices = choices;
218 this.options = Object.assign({
221 orientation: 'horizontal'
227 keys = Object.keys(this.choices);
229 if (this.options.sort === true)
231 else if (Array.isArray(this.options.sort))
232 keys = this.options.sort;
234 if (this.options.widget == 'select') {
235 frameEl = E('select', {
236 'id': this.options.id,
237 'name': this.options.name,
238 'size': this.options.size,
239 'class': 'cbi-input-select',
240 'multiple': this.options.multi ? '' : null
243 if (this.options.optional)
244 frameEl.appendChild(E('option', {
246 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
247 }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
249 for (var i = 0; i < keys.length; i++) {
250 if (keys[i] == null || keys[i] == '')
253 frameEl.appendChild(E('option', {
255 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
256 }, this.choices[keys[i]] || keys[i]));
261 'id': this.options.id
264 var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
266 for (var i = 0; i < keys.length; i++) {
267 frameEl.appendChild(E('label', {}, [
269 'name': this.options.id || this.options.name,
270 'type': this.options.multi ? 'checkbox' : 'radio',
271 'class': this.options.multi ? 'cbi-input-checkbox' : 'cbi-input-radio',
273 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
275 this.choices[keys[i]] || keys[i]
278 if (i + 1 == this.options.size)
279 frameEl.appendChild(brEl);
283 return this.bind(frameEl);
286 bind: function(frameEl) {
289 if (this.options.widget == 'select') {
290 this.setUpdateEvents(frameEl, 'change', 'click', 'blur');
291 this.setChangeEvents(frameEl, 'change');
294 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
295 for (var i = 0; i < radioEls.length; i++) {
296 this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
297 this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
301 L.dom.bindClassInstance(frameEl, this);
306 getValue: function() {
307 if (this.options.widget == 'select')
308 return this.node.value;
310 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
311 for (var i = 0; i < radioEls.length; i++)
312 if (radioEls[i].checked)
313 return radioEls[i].value;
318 setValue: function(value) {
319 if (this.options.widget == 'select') {
323 for (var i = 0; i < this.node.options.length; i++)
324 this.node.options[i].selected = (this.node.options[i].value == value);
329 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
330 for (var i = 0; i < radioEls.length; i++)
331 radioEls[i].checked = (radioEls[i].value == value);
335 var UIDropdown = UIElement.extend({
336 __init__: function(value, choices, options) {
337 if (typeof(choices) != 'object')
340 if (!Array.isArray(value))
341 this.values = (value != null && value != '') ? [ value ] : [];
345 this.choices = choices;
346 this.options = Object.assign({
348 multi: Array.isArray(value),
350 select_placeholder: _('-- Please choose --'),
351 custom_placeholder: _('-- custom --'),
355 create_query: '.create-item-input',
356 create_template: 'script[type="item-template"]'
362 'id': this.options.id,
363 'class': 'cbi-dropdown',
364 'multiple': this.options.multi ? '' : null,
365 'optional': this.options.optional ? '' : null,
368 var keys = Object.keys(this.choices);
370 if (this.options.sort === true)
372 else if (Array.isArray(this.options.sort))
373 keys = this.options.sort;
375 if (this.options.create)
376 for (var i = 0; i < this.values.length; i++)
377 if (!this.choices.hasOwnProperty(this.values[i]))
378 keys.push(this.values[i]);
380 for (var i = 0; i < keys.length; i++)
381 sb.lastElementChild.appendChild(E('li', {
382 'data-value': keys[i],
383 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
384 }, this.choices[keys[i]] || keys[i]));
386 if (this.options.create) {
387 var createEl = E('input', {
389 'class': 'create-item-input',
390 'readonly': this.options.readonly ? '' : null,
391 'maxlength': this.options.maxlength,
392 'placeholder': this.options.custom_placeholder || this.options.placeholder
395 if (this.options.datatype)
396 L.ui.addValidator(createEl, this.options.datatype,
397 true, null, 'blur', 'keyup');
399 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
402 if (this.options.create_markup)
403 sb.appendChild(E('script', { type: 'item-template' },
404 this.options.create_markup));
406 return this.bind(sb);
410 var o = this.options;
412 o.multi = sb.hasAttribute('multiple');
413 o.optional = sb.hasAttribute('optional');
414 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
415 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
416 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
417 o.create_query = sb.getAttribute('item-create') || o.create_query;
418 o.create_template = sb.getAttribute('item-template') || o.create_template;
420 var ul = sb.querySelector('ul'),
421 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
422 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
423 canary = sb.appendChild(E('div')),
424 create = sb.querySelector(this.options.create_query),
425 ndisplay = this.options.display_items,
428 if (this.options.multi) {
429 var items = ul.querySelectorAll('li');
431 for (var i = 0; i < items.length; i++) {
432 this.transformItem(sb, items[i]);
434 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
435 items[i].setAttribute('display', n++);
439 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
440 var placeholder = E('li', { placeholder: '' },
441 this.options.select_placeholder || this.options.placeholder);
444 ? ul.insertBefore(placeholder, ul.firstChild)
445 : ul.appendChild(placeholder);
448 var items = ul.querySelectorAll('li'),
449 sel = sb.querySelectorAll('[selected]');
451 sel.forEach(function(s) {
452 s.removeAttribute('selected');
455 var s = sel[0] || items[0];
457 s.setAttribute('selected', '');
458 s.setAttribute('display', n++);
464 this.saveValues(sb, ul);
466 ul.setAttribute('tabindex', -1);
467 sb.setAttribute('tabindex', 0);
470 sb.setAttribute('more', '')
472 sb.removeAttribute('more');
474 if (ndisplay == this.options.display_items)
475 sb.setAttribute('empty', '')
477 sb.removeAttribute('empty');
479 more.innerHTML = (ndisplay == this.options.display_items)
480 ? (this.options.select_placeholder || this.options.placeholder) : '···';
483 sb.addEventListener('click', this.handleClick.bind(this));
484 sb.addEventListener('keydown', this.handleKeydown.bind(this));
485 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
486 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
488 if ('ontouchstart' in window) {
489 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
490 window.addEventListener('touchstart', this.closeAllDropdowns);
493 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
494 sb.addEventListener('focus', this.handleFocus.bind(this));
496 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
498 window.addEventListener('mouseover', this.setFocus);
499 window.addEventListener('click', this.closeAllDropdowns);
503 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
504 create.addEventListener('focus', this.handleCreateFocus.bind(this));
505 create.addEventListener('blur', this.handleCreateBlur.bind(this));
507 var li = findParent(create, 'li');
509 li.setAttribute('unselectable', '');
510 li.addEventListener('click', this.handleCreateClick.bind(this));
515 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
516 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
518 L.dom.bindClassInstance(sb, this);
523 openDropdown: function(sb) {
524 var st = window.getComputedStyle(sb, null),
525 ul = sb.querySelector('ul'),
526 li = ul.querySelectorAll('li'),
527 fl = findParent(sb, '.cbi-value-field'),
528 sel = ul.querySelector('[selected]'),
529 rect = sb.getBoundingClientRect(),
530 items = Math.min(this.options.dropdown_items, li.length);
532 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
533 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
536 sb.setAttribute('open', '');
538 var pv = ul.cloneNode(true);
539 pv.classList.add('preview');
542 fl.classList.add('cbi-dropdown-open');
544 if ('ontouchstart' in window) {
545 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
546 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
547 scrollFrom = window.pageYOffset,
548 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
551 ul.style.top = sb.offsetHeight + 'px';
552 ul.style.left = -rect.left + 'px';
553 ul.style.right = (rect.right - vpWidth) + 'px';
554 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
555 ul.style.WebkitOverflowScrolling = 'touch';
557 var scrollStep = function(timestamp) {
560 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
563 var duration = Math.max(timestamp - start, 1);
564 if (duration < 100) {
565 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
566 window.requestAnimationFrame(scrollStep);
569 document.body.scrollTop = scrollTo;
573 window.requestAnimationFrame(scrollStep);
576 ul.style.maxHeight = '1px';
577 ul.style.top = ul.style.bottom = '';
579 window.requestAnimationFrame(function() {
580 var height = items * li[Math.max(0, li.length - 2)].offsetHeight;
582 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
583 ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
584 ul.style.maxHeight = height + 'px';
588 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
589 for (var i = 0; i < cboxes.length; i++) {
590 cboxes[i].checked = true;
591 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
594 ul.classList.add('dropdown');
596 sb.insertBefore(pv, ul.nextElementSibling);
598 li.forEach(function(l) {
599 l.setAttribute('tabindex', 0);
602 sb.lastElementChild.setAttribute('tabindex', 0);
604 this.setFocus(sb, sel || li[0], true);
607 closeDropdown: function(sb, no_focus) {
608 if (!sb.hasAttribute('open'))
611 var pv = sb.querySelector('ul.preview'),
612 ul = sb.querySelector('ul.dropdown'),
613 li = ul.querySelectorAll('li'),
614 fl = findParent(sb, '.cbi-value-field');
616 li.forEach(function(l) { l.removeAttribute('tabindex'); });
617 sb.lastElementChild.removeAttribute('tabindex');
620 sb.removeAttribute('open');
621 sb.style.width = sb.style.height = '';
623 ul.classList.remove('dropdown');
624 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
627 fl.classList.remove('cbi-dropdown-open');
630 this.setFocus(sb, sb);
632 this.saveValues(sb, ul);
635 toggleItem: function(sb, li, force_state) {
636 if (li.hasAttribute('unselectable'))
639 if (this.options.multi) {
640 var cbox = li.querySelector('input[type="checkbox"]'),
641 items = li.parentNode.querySelectorAll('li'),
642 label = sb.querySelector('ul.preview'),
643 sel = li.parentNode.querySelectorAll('[selected]').length,
644 more = sb.querySelector('.more'),
645 ndisplay = this.options.display_items,
648 if (li.hasAttribute('selected')) {
649 if (force_state !== true) {
650 if (sel > 1 || this.options.optional) {
651 li.removeAttribute('selected');
652 cbox.checked = cbox.disabled = false;
656 cbox.disabled = true;
661 if (force_state !== false) {
662 li.setAttribute('selected', '');
664 cbox.disabled = false;
669 while (label && label.firstElementChild)
670 label.removeChild(label.firstElementChild);
672 for (var i = 0; i < items.length; i++) {
673 items[i].removeAttribute('display');
674 if (items[i].hasAttribute('selected')) {
675 if (ndisplay-- > 0) {
676 items[i].setAttribute('display', n++);
678 label.appendChild(items[i].cloneNode(true));
680 var c = items[i].querySelector('input[type="checkbox"]');
682 c.disabled = (sel == 1 && !this.options.optional);
687 sb.setAttribute('more', '');
689 sb.removeAttribute('more');
691 if (ndisplay === this.options.display_items)
692 sb.setAttribute('empty', '');
694 sb.removeAttribute('empty');
696 more.innerHTML = (ndisplay === this.options.display_items)
697 ? (this.options.select_placeholder || this.options.placeholder) : '···';
700 var sel = li.parentNode.querySelector('[selected]');
702 sel.removeAttribute('display');
703 sel.removeAttribute('selected');
706 li.setAttribute('display', 0);
707 li.setAttribute('selected', '');
709 this.closeDropdown(sb, true);
712 this.saveValues(sb, li.parentNode);
715 transformItem: function(sb, li) {
716 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
719 while (li.firstChild)
720 label.appendChild(li.firstChild);
722 li.appendChild(cbox);
723 li.appendChild(label);
726 saveValues: function(sb, ul) {
727 var sel = ul.querySelectorAll('li[selected]'),
728 div = sb.lastElementChild,
729 name = this.options.name,
733 while (div.lastElementChild)
734 div.removeChild(div.lastElementChild);
736 sel.forEach(function (s) {
737 if (s.hasAttribute('placeholder'))
742 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
746 div.appendChild(E('input', {
754 strval += strval.length ? ' ' + v.value : v.value;
762 if (this.options.multi)
763 detail.values = values;
765 detail.value = values.length ? values[0] : null;
769 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
775 setValues: function(sb, values) {
776 var ul = sb.querySelector('ul');
778 if (this.options.create) {
779 for (var value in values) {
780 this.createItems(sb, value);
782 if (!this.options.multi)
787 if (this.options.multi) {
788 var lis = ul.querySelectorAll('li[data-value]');
789 for (var i = 0; i < lis.length; i++) {
790 var value = lis[i].getAttribute('data-value');
791 if (values === null || !(value in values))
792 this.toggleItem(sb, lis[i], false);
794 this.toggleItem(sb, lis[i], true);
798 var ph = ul.querySelector('li[placeholder]');
800 this.toggleItem(sb, ph);
802 var lis = ul.querySelectorAll('li[data-value]');
803 for (var i = 0; i < lis.length; i++) {
804 var value = lis[i].getAttribute('data-value');
805 if (values !== null && (value in values))
806 this.toggleItem(sb, lis[i]);
811 setFocus: function(sb, elem, scroll) {
812 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
815 if (sb.target && findParent(sb.target, 'ul.dropdown'))
818 document.querySelectorAll('.focus').forEach(function(e) {
819 if (!matchesElem(e, 'input')) {
820 e.classList.remove('focus');
827 elem.classList.add('focus');
830 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
834 createItems: function(sb, value) {
836 val = (value || '').trim(),
837 ul = sb.querySelector('ul');
839 if (!sbox.options.multi)
840 val = val.length ? [ val ] : [];
842 val = val.length ? val.split(/\s+/) : [];
844 val.forEach(function(item) {
847 ul.childNodes.forEach(function(li) {
848 if (li.getAttribute && li.getAttribute('data-value') === item)
854 tpl = sb.querySelector(sbox.options.create_template);
857 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
859 markup = '<li data-value="{{value}}">{{value}}</li>';
861 new_item = E(markup.replace(/{{value}}/g, item));
863 if (sbox.options.multi) {
864 sbox.transformItem(sb, new_item);
867 var old = ul.querySelector('li[created]');
871 new_item.setAttribute('created', '');
874 new_item = ul.insertBefore(new_item, ul.lastElementChild);
877 sbox.toggleItem(sb, new_item, true);
878 sbox.setFocus(sb, new_item, true);
882 closeAllDropdowns: function() {
883 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
884 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
888 handleClick: function(ev) {
889 var sb = ev.currentTarget;
891 if (!sb.hasAttribute('open')) {
892 if (!matchesElem(ev.target, 'input'))
893 this.openDropdown(sb);
896 var li = findParent(ev.target, 'li');
897 if (li && li.parentNode.classList.contains('dropdown'))
898 this.toggleItem(sb, li);
899 else if (li && li.parentNode.classList.contains('preview'))
900 this.closeDropdown(sb);
904 ev.stopPropagation();
907 handleKeydown: function(ev) {
908 var sb = ev.currentTarget;
910 if (matchesElem(ev.target, 'input'))
913 if (!sb.hasAttribute('open')) {
914 switch (ev.keyCode) {
919 this.openDropdown(sb);
924 var active = findParent(document.activeElement, 'li');
926 switch (ev.keyCode) {
928 this.closeDropdown(sb);
933 if (!active.hasAttribute('selected'))
934 this.toggleItem(sb, active);
935 this.closeDropdown(sb);
942 this.toggleItem(sb, active);
948 if (active && active.previousElementSibling) {
949 this.setFocus(sb, active.previousElementSibling);
955 if (active && active.nextElementSibling) {
956 this.setFocus(sb, active.nextElementSibling);
964 handleDropdownClose: function(ev) {
965 var sb = ev.currentTarget;
967 this.closeDropdown(sb, true);
970 handleDropdownSelect: function(ev) {
971 var sb = ev.currentTarget,
972 li = findParent(ev.target, 'li');
977 this.toggleItem(sb, li);
978 this.closeDropdown(sb, true);
981 handleMouseover: function(ev) {
982 var sb = ev.currentTarget;
984 if (!sb.hasAttribute('open'))
987 var li = findParent(ev.target, 'li');
989 if (li && li.parentNode.classList.contains('dropdown'))
990 this.setFocus(sb, li);
993 handleFocus: function(ev) {
994 var sb = ev.currentTarget;
996 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
997 if (s !== sb || sb.hasAttribute('open'))
998 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1002 handleCanaryFocus: function(ev) {
1003 this.closeDropdown(ev.currentTarget.parentNode);
1006 handleCreateKeydown: function(ev) {
1007 var input = ev.currentTarget,
1008 sb = findParent(input, '.cbi-dropdown');
1010 switch (ev.keyCode) {
1012 ev.preventDefault();
1014 if (input.classList.contains('cbi-input-invalid'))
1017 this.createItems(sb, input.value);
1024 handleCreateFocus: function(ev) {
1025 var input = ev.currentTarget,
1026 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1027 sb = findParent(input, '.cbi-dropdown');
1030 cbox.checked = true;
1032 sb.setAttribute('locked-in', '');
1035 handleCreateBlur: function(ev) {
1036 var input = ev.currentTarget,
1037 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1038 sb = findParent(input, '.cbi-dropdown');
1041 cbox.checked = false;
1043 sb.removeAttribute('locked-in');
1046 handleCreateClick: function(ev) {
1047 ev.currentTarget.querySelector(this.options.create_query).focus();
1050 setValue: function(values) {
1051 if (this.options.multi) {
1052 if (!Array.isArray(values))
1053 values = (values != null && values != '') ? [ values ] : [];
1057 for (var i = 0; i < values.length; i++)
1058 v[values[i]] = true;
1060 this.setValues(this.node, v);
1065 if (values != null) {
1066 if (Array.isArray(values))
1067 v[values[0]] = true;
1072 this.setValues(this.node, v);
1076 getValue: function() {
1077 var div = this.node.lastElementChild,
1078 h = div.querySelectorAll('input[type="hidden"]'),
1081 for (var i = 0; i < h.length; i++)
1084 return this.options.multi ? v : v[0];
1088 var UICombobox = UIDropdown.extend({
1089 __init__: function(value, choices, options) {
1090 this.super('__init__', [ value, choices, Object.assign({
1091 select_placeholder: _('-- Please choose --'),
1092 custom_placeholder: _('-- custom --'),
1103 var UIDynamicList = UIElement.extend({
1104 __init__: function(values, choices, options) {
1105 if (!Array.isArray(values))
1106 values = (values != null && values != '') ? [ values ] : [];
1108 if (typeof(choices) != 'object')
1111 this.values = values;
1112 this.choices = choices;
1113 this.options = Object.assign({}, options, {
1119 render: function() {
1121 'id': this.options.id,
1122 'class': 'cbi-dynlist'
1123 }, E('div', { 'class': 'add-item' }));
1126 var cbox = new UICombobox(null, this.choices, this.options);
1127 dl.lastElementChild.appendChild(cbox.render());
1130 var inputEl = E('input', {
1132 'class': 'cbi-input-text',
1133 'placeholder': this.options.placeholder
1136 dl.lastElementChild.appendChild(inputEl);
1137 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1139 if (this.options.datatype)
1140 L.ui.addValidator(inputEl, this.options.datatype,
1141 true, null, 'blur', 'keyup');
1144 for (var i = 0; i < this.values.length; i++)
1145 this.addItem(dl, this.values[i],
1146 this.choices ? this.choices[this.values[i]] : null);
1148 return this.bind(dl);
1151 bind: function(dl) {
1152 dl.addEventListener('click', L.bind(this.handleClick, this));
1153 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
1154 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
1158 this.setUpdateEvents(dl, 'cbi-dynlist-change');
1159 this.setChangeEvents(dl, 'cbi-dynlist-change');
1161 L.dom.bindClassInstance(dl, this);
1166 addItem: function(dl, value, text, flash) {
1168 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
1169 E('span', {}, text || value),
1172 'name': this.options.name,
1173 'value': value })]);
1175 dl.querySelectorAll('.item, .add-item').forEach(function(item) {
1179 var hidden = item.querySelector('input[type="hidden"]');
1181 if (hidden && hidden.parentNode !== item)
1184 if (hidden && hidden.value === value)
1186 else if (!hidden || hidden.value >= value)
1187 exists = !!item.parentNode.insertBefore(new_item, item);
1190 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1201 removeItem: function(dl, item) {
1202 var value = item.querySelector('input[type="hidden"]').value;
1203 var sb = dl.querySelector('.cbi-dropdown');
1205 sb.querySelectorAll('ul > li').forEach(function(li) {
1206 if (li.getAttribute('data-value') === value) {
1207 if (li.hasAttribute('dynlistcustom'))
1208 li.parentNode.removeChild(li);
1210 li.removeAttribute('unselectable');
1214 item.parentNode.removeChild(item);
1216 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1227 handleClick: function(ev) {
1228 var dl = ev.currentTarget,
1229 item = findParent(ev.target, '.item');
1232 this.removeItem(dl, item);
1234 else if (matchesElem(ev.target, '.cbi-button-add')) {
1235 var input = ev.target.previousElementSibling;
1236 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
1237 this.addItem(dl, input.value, null, true);
1243 handleDropdownChange: function(ev) {
1244 var dl = ev.currentTarget,
1245 sbIn = ev.detail.instance,
1246 sbEl = ev.detail.element,
1247 sbVal = ev.detail.value;
1252 sbIn.setValues(sbEl, null);
1253 sbVal.element.setAttribute('unselectable', '');
1255 if (sbVal.element.hasAttribute('created')) {
1256 sbVal.element.removeAttribute('created');
1257 sbVal.element.setAttribute('dynlistcustom', '');
1260 this.addItem(dl, sbVal.value, sbVal.text, true);
1263 handleKeydown: function(ev) {
1264 var dl = ev.currentTarget,
1265 item = findParent(ev.target, '.item');
1268 switch (ev.keyCode) {
1269 case 8: /* backspace */
1270 if (item.previousElementSibling)
1271 item.previousElementSibling.focus();
1273 this.removeItem(dl, item);
1276 case 46: /* delete */
1277 if (item.nextElementSibling) {
1278 if (item.nextElementSibling.classList.contains('item'))
1279 item.nextElementSibling.focus();
1281 item.nextElementSibling.firstElementChild.focus();
1284 this.removeItem(dl, item);
1288 else if (matchesElem(ev.target, '.cbi-input-text')) {
1289 switch (ev.keyCode) {
1290 case 13: /* enter */
1291 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
1292 this.addItem(dl, ev.target.value, null, true);
1293 ev.target.value = '';
1298 ev.preventDefault();
1304 getValue: function() {
1305 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1308 for (var i = 0; i < items.length; i++)
1309 v.push(items[i].value);
1314 setValue: function(values) {
1315 if (!Array.isArray(values))
1316 values = (values != null && values != '') ? [ values ] : [];
1318 var items = this.node.querySelectorAll('.item');
1320 for (var i = 0; i < items.length; i++)
1321 if (items[i].parentNode === this.node)
1322 this.removeItem(this.node, items[i]);
1324 for (var i = 0; i < values.length; i++)
1325 this.addItem(this.node, values[i],
1326 this.choices ? this.choices[values[i]] : null);
1330 var UIHiddenfield = UIElement.extend({
1331 __init__: function(value, options) {
1333 this.options = Object.assign({
1338 render: function() {
1339 var hiddenEl = E('input', {
1340 'id': this.options.id,
1345 return this.bind(hiddenEl);
1348 bind: function(hiddenEl) {
1349 this.node = hiddenEl;
1351 L.dom.bindClassInstance(hiddenEl, this);
1356 getValue: function() {
1357 return this.node.value;
1360 setValue: function(value) {
1361 this.node.value = value;
1366 return L.Class.extend({
1367 __init__: function() {
1368 modalDiv = document.body.appendChild(
1369 L.dom.create('div', { id: 'modal_overlay' },
1370 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1372 tooltipDiv = document.body.appendChild(
1373 L.dom.create('div', { class: 'cbi-tooltip' }));
1375 /* setup old aliases */
1376 L.showModal = this.showModal;
1377 L.hideModal = this.hideModal;
1378 L.showTooltip = this.showTooltip;
1379 L.hideTooltip = this.hideTooltip;
1380 L.itemlist = this.itemlist;
1382 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1383 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1384 document.addEventListener('focus', this.showTooltip.bind(this), true);
1385 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1387 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1388 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
1389 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
1393 showModal: function(title, children) {
1394 var dlg = modalDiv.firstElementChild;
1396 dlg.setAttribute('class', 'modal');
1398 L.dom.content(dlg, L.dom.create('h4', {}, title));
1399 L.dom.append(dlg, children);
1401 document.body.classList.add('modal-overlay-active');
1406 hideModal: function() {
1407 document.body.classList.remove('modal-overlay-active');
1411 showTooltip: function(ev) {
1412 var target = findParent(ev.target, '[data-tooltip]');
1417 if (tooltipTimeout !== null) {
1418 window.clearTimeout(tooltipTimeout);
1419 tooltipTimeout = null;
1422 var rect = target.getBoundingClientRect(),
1423 x = rect.left + window.pageXOffset,
1424 y = rect.top + rect.height + window.pageYOffset;
1426 tooltipDiv.className = 'cbi-tooltip';
1427 tooltipDiv.innerHTML = '▲ ';
1428 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1430 if (target.hasAttribute('data-tooltip-style'))
1431 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1433 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1434 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1435 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1438 tooltipDiv.style.top = y + 'px';
1439 tooltipDiv.style.left = x + 'px';
1440 tooltipDiv.style.opacity = 1;
1442 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1444 detail: { target: target }
1448 hideTooltip: function(ev) {
1449 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1450 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1453 if (tooltipTimeout !== null) {
1454 window.clearTimeout(tooltipTimeout);
1455 tooltipTimeout = null;
1458 tooltipDiv.style.opacity = 0;
1459 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1461 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1465 itemlist: function(node, items, separators) {
1468 if (!Array.isArray(separators))
1469 separators = [ separators || E('br') ];
1471 for (var i = 0; i < items.length; i += 2) {
1472 if (items[i+1] !== null && items[i+1] !== undefined) {
1473 var sep = separators[(i/2) % separators.length],
1476 children.push(E('span', { class: 'nowrap' }, [
1477 items[i] ? E('strong', items[i] + ': ') : '',
1481 if ((i+2) < items.length)
1482 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
1486 L.dom.content(node, children);
1492 tabs: L.Class.singleton({
1494 var groups = [], prevGroup = null, currGroup = null;
1496 document.querySelectorAll('[data-tab]').forEach(function(tab) {
1497 var parent = tab.parentNode;
1499 if (!parent.hasAttribute('data-tab-group'))
1500 parent.setAttribute('data-tab-group', groups.length);
1502 currGroup = +parent.getAttribute('data-tab-group');
1504 if (currGroup !== prevGroup) {
1505 prevGroup = currGroup;
1507 if (!groups[currGroup])
1508 groups[currGroup] = [];
1511 groups[currGroup].push(tab);
1514 for (var i = 0; i < groups.length; i++)
1515 this.initTabGroup(groups[i]);
1517 document.addEventListener('dependency-update', this.updateTabs.bind(this));
1522 this.setActiveTabId(-1, -1);
1525 initTabGroup: function(panes) {
1526 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
1529 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
1530 group = panes[0].parentNode,
1531 groupId = +group.getAttribute('data-tab-group'),
1534 for (var i = 0, pane; pane = panes[i]; i++) {
1535 var name = pane.getAttribute('data-tab'),
1536 title = pane.getAttribute('data-tab-title'),
1537 active = pane.getAttribute('data-tab-active') === 'true';
1539 menu.appendChild(E('li', {
1540 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
1544 'click': this.switchTab.bind(this)
1551 group.parentNode.insertBefore(menu, group);
1553 if (selected === null) {
1554 selected = this.getActiveTabId(groupId);
1556 if (selected < 0 || selected >= panes.length)
1559 menu.childNodes[selected].classList.add('cbi-tab');
1560 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
1561 panes[selected].setAttribute('data-tab-active', 'true');
1563 this.setActiveTabId(groupId, selected);
1567 getActiveTabState: function() {
1568 var page = document.body.getAttribute('data-page');
1571 var val = JSON.parse(window.sessionStorage.getItem('tab'));
1572 if (val.page === page && Array.isArray(val.groups))
1577 window.sessionStorage.removeItem('tab');
1578 return { page: page, groups: [] };
1581 getActiveTabId: function(groupId) {
1582 return +this.getActiveTabState().groups[groupId] || 0;
1585 setActiveTabId: function(groupId, tabIndex) {
1587 var state = this.getActiveTabState();
1588 state.groups[groupId] = tabIndex;
1590 window.sessionStorage.setItem('tab', JSON.stringify(state));
1592 catch (e) { return false; }
1597 updateTabs: function(ev) {
1598 document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
1599 var menu = pane.parentNode.previousElementSibling,
1600 tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
1601 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
1603 if (!pane.firstElementChild) {
1604 tab.style.display = 'none';
1605 tab.classList.remove('flash');
1607 else if (tab.style.display === 'none') {
1608 tab.style.display = '';
1609 requestAnimationFrame(function() { tab.classList.add('flash') });
1613 tab.setAttribute('data-errors', n_errors);
1614 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
1615 tab.setAttribute('data-tooltip-style', 'error');
1618 tab.removeAttribute('data-errors');
1619 tab.removeAttribute('data-tooltip');
1624 switchTab: function(ev) {
1625 var tab = ev.target.parentNode,
1626 name = tab.getAttribute('data-tab'),
1627 menu = tab.parentNode,
1628 group = menu.nextElementSibling,
1629 groupId = +group.getAttribute('data-tab-group'),
1632 ev.preventDefault();
1634 if (!tab.classList.contains('cbi-tab-disabled'))
1637 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
1638 tab.classList.remove('cbi-tab');
1639 tab.classList.remove('cbi-tab-disabled');
1641 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
1644 group.childNodes.forEach(function(pane) {
1645 if (L.dom.matches(pane, '[data-tab]')) {
1646 if (pane.getAttribute('data-tab') === name) {
1647 pane.setAttribute('data-tab-active', 'true');
1648 L.ui.tabs.setActiveTabId(groupId, index);
1651 pane.setAttribute('data-tab-active', 'false');
1661 changes: L.Class.singleton({
1663 if (!L.env.sessionid)
1666 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
1669 setIndicator: function(n) {
1670 var i = document.querySelector('.uci_change_indicator');
1672 var poll = document.getElementById('xhr_poll_status');
1673 i = poll.parentNode.insertBefore(E('a', {
1675 'class': 'uci_change_indicator label notice',
1676 'click': L.bind(this.displayChanges, this)
1681 L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
1682 i.classList.add('flash');
1683 i.style.display = '';
1686 i.classList.remove('flash');
1687 i.style.display = 'none';
1691 renderChangeIndicator: function(changes) {
1694 for (var config in changes)
1695 if (changes.hasOwnProperty(config))
1696 n_changes += changes[config].length;
1698 this.changes = changes;
1699 this.setIndicator(n_changes);
1703 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1704 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1705 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1706 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1707 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1708 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1709 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1710 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1711 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1712 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1715 displayChanges: function() {
1716 var list = E('div', { 'class': 'uci-change-list' }),
1717 dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
1718 E('div', { 'class': 'cbi-section' }, [
1719 E('strong', _('Legend:')),
1720 E('div', { 'class': 'uci-change-legend' }, [
1721 E('div', { 'class': 'uci-change-legend-label' }, [
1722 E('ins', ' '), ' ', _('Section added') ]),
1723 E('div', { 'class': 'uci-change-legend-label' }, [
1724 E('del', ' '), ' ', _('Section removed') ]),
1725 E('div', { 'class': 'uci-change-legend-label' }, [
1726 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1727 E('div', { 'class': 'uci-change-legend-label' }, [
1728 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1730 E('div', { 'class': 'right' }, [
1734 'click': L.ui.hideModal,
1735 'value': _('Dismiss')
1739 'class': 'cbi-button cbi-button-positive important',
1740 'click': L.bind(this.apply, this, true),
1741 'value': _('Save & Apply')
1745 'class': 'cbi-button cbi-button-reset',
1746 'click': L.bind(this.revert, this),
1747 'value': _('Revert')
1751 for (var config in this.changes) {
1752 if (!this.changes.hasOwnProperty(config))
1755 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
1757 for (var i = 0, added = null; i < this.changes[config].length; i++) {
1758 var chg = this.changes[config][i],
1759 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
1761 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
1767 if (added != null && chg[1] == added[0])
1768 return '@' + added[1] + '[-1]';
1773 return "'" + chg[3].replace(/'/g, "'\"'\"'") + "'";
1780 if (chg[0] == 'add')
1781 added = [ chg[1], chg[2] ];
1785 list.appendChild(E('br'));
1786 dlg.classList.add('uci-dialog');
1789 displayStatus: function(type, content) {
1791 var message = L.ui.showModal('', '');
1793 message.classList.add('alert-message');
1794 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1797 L.dom.content(message, content);
1799 if (!this.was_polling) {
1800 this.was_polling = L.Request.poll.active();
1801 L.Request.poll.stop();
1807 if (this.was_polling)
1808 L.Request.poll.start();
1812 rollback: function(checked) {
1814 this.displayStatus('warning spinning',
1815 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
1816 .format(L.env.apply_rollback)));
1818 var call = function(r, data, duration) {
1819 if (r.status === 204) {
1820 L.ui.changes.displayStatus('warning', [
1821 E('h4', _('Configuration has been rolled back!')),
1822 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)),
1823 E('div', { 'class': 'right' }, [
1827 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1828 'value': _('Dismiss')
1832 'class': 'btn cbi-button-action important',
1833 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1834 'value': _('Revert changes')
1838 'class': 'btn cbi-button-negative important',
1839 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1840 'value': _('Apply unchecked')
1848 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1849 window.setTimeout(function() {
1850 L.Request.request(L.url('admin/uci/confirm'), {
1852 timeout: L.env.apply_timeout * 1000,
1853 query: { sid: L.env.sessionid, token: L.env.token }
1858 call({ status: 0 });
1861 this.displayStatus('warning', [
1862 E('h4', _('Device unreachable!')),
1863 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.'))
1868 confirm: function(checked, deadline, override_token) {
1870 var ts = Date.now();
1872 this.displayStatus('notice');
1875 this.confirm_auth = { token: override_token };
1877 var call = function(r, data, duration) {
1878 if (Date.now() >= deadline) {
1879 window.clearTimeout(tt);
1880 L.ui.changes.rollback(checked);
1883 else if (r && (r.status === 200 || r.status === 204)) {
1884 document.dispatchEvent(new CustomEvent('uci-applied'));
1886 L.ui.changes.setIndicator(0);
1887 L.ui.changes.displayStatus('notice',
1888 E('p', _('Configuration has been applied.')));
1890 window.clearTimeout(tt);
1891 window.setTimeout(function() {
1892 //L.ui.changes.displayStatus(false);
1893 window.location = window.location.href.split('#')[0];
1894 }, L.env.apply_display * 1000);
1899 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1900 window.setTimeout(function() {
1901 L.Request.request(L.url('admin/uci/confirm'), {
1903 timeout: L.env.apply_timeout * 1000,
1904 query: L.ui.changes.confirm_auth
1909 var tick = function() {
1910 var now = Date.now();
1912 L.ui.changes.displayStatus('notice spinning',
1913 E('p', _('Waiting for configuration to get applied… %ds')
1914 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
1916 if (now >= deadline)
1919 tt = window.setTimeout(tick, 1000 - (now - ts));
1925 /* wait a few seconds for the settings to become effective */
1926 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
1929 apply: function(checked) {
1930 this.displayStatus('notice spinning',
1931 E('p', _('Starting configuration apply…')));
1933 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
1935 query: { sid: L.env.sessionid, token: L.env.token }
1936 }).then(function(r) {
1937 if (r.status === (checked ? 200 : 204)) {
1938 var tok = null; try { tok = r.json(); } catch(e) {}
1939 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
1940 L.ui.changes.confirm_auth = tok;
1942 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
1944 else if (checked && r.status === 204) {
1945 L.ui.changes.displayStatus('notice',
1946 E('p', _('There are no changes to apply')));
1948 window.setTimeout(function() {
1949 L.ui.changes.displayStatus(false);
1950 }, L.env.apply_display * 1000);
1953 L.ui.changes.displayStatus('warning',
1954 E('p', _('Apply request failed with status <code>%h</code>%>')
1955 .format(r.responseText || r.statusText || r.status)));
1957 window.setTimeout(function() {
1958 L.ui.changes.displayStatus(false);
1959 }, L.env.apply_display * 1000);
1964 revert: function() {
1965 this.displayStatus('notice spinning',
1966 E('p', _('Reverting configuration…')));
1968 L.Request.request(L.url('admin/uci/revert'), {
1970 query: { sid: L.env.sessionid, token: L.env.token }
1971 }).then(function(r) {
1972 if (r.status === 200) {
1973 document.dispatchEvent(new CustomEvent('uci-reverted'));
1975 L.ui.changes.setIndicator(0);
1976 L.ui.changes.displayStatus('notice',
1977 E('p', _('Changes have been reverted.')));
1979 window.setTimeout(function() {
1980 //L.ui.changes.displayStatus(false);
1981 window.location = window.location.href.split('#')[0];
1982 }, L.env.apply_display * 1000);
1985 L.ui.changes.displayStatus('warning',
1986 E('p', _('Revert request failed with status <code>%h</code>')
1987 .format(r.statusText || r.status)));
1989 window.setTimeout(function() {
1990 L.ui.changes.displayStatus(false);
1991 }, L.env.apply_display * 1000);
1997 addValidator: function(field, type, optional, vfunc /*, ... */) {
2001 var events = this.varargs(arguments, 3);
2002 if (events.length == 0)
2003 events.push('blur', 'keyup');
2006 var cbiValidator = new CBIValidator(field, type, optional, vfunc),
2007 validatorFn = cbiValidator.validate.bind(cbiValidator);
2009 for (var i = 0; i < events.length; i++)
2010 field.addEventListener(events[i], validatorFn);
2020 Textfield: UITextfield,
2021 Checkbox: UICheckbox,
2023 Dropdown: UIDropdown,
2024 DynamicList: UIDynamicList,
2025 Combobox: UICombobox,
2026 Hiddenfield: UIHiddenfield