9 var UIElement = L.Class.extend({
10 getValue: function() {
11 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
12 return this.node.value;
17 setValue: function(value) {
18 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
19 this.node.value = value;
23 return (this.validState !== false);
26 triggerValidation: function() {
27 if (typeof(this.vfunc) != 'function')
30 var wasValid = this.isValid();
34 return (wasValid != this.isValid());
37 registerEvents: function(targetNode, synevent, events) {
38 var dispatchFn = L.bind(function(ev) {
39 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
42 for (var i = 0; i < events.length; i++)
43 targetNode.addEventListener(events[i], dispatchFn);
46 setUpdateEvents: function(targetNode /*, ... */) {
47 var datatype = this.options.datatype,
48 optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
49 validate = this.options.validate,
50 events = this.varargs(arguments, 1);
52 this.registerEvents(targetNode, 'widget-update', events);
54 if (!datatype && !validate)
57 this.vfunc = L.ui.addValidator.apply(L.ui, [
58 targetNode, datatype || 'string',
62 this.node.addEventListener('validation-success', L.bind(function(ev) {
63 this.validState = true;
66 this.node.addEventListener('validation-failure', L.bind(function(ev) {
67 this.validState = false;
71 setChangeEvents: function(targetNode /*, ... */) {
72 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
76 var UITextfield = UIElement.extend({
77 __init__: function(value, options) {
79 this.options = Object.assign({
86 var frameEl = E('div', { 'id': this.options.id });
88 if (this.options.password) {
89 frameEl.classList.add('nowrap');
90 frameEl.appendChild(E('input', {
92 'style': 'position:absolute; left:-100000px',
95 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
99 frameEl.appendChild(E('input', {
100 'id': this.options.id ? 'widget.' + this.options.id : null,
101 'name': this.options.name,
102 'type': this.options.password ? 'password' : 'text',
103 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
104 'readonly': this.options.readonly ? '' : null,
105 'maxlength': this.options.maxlength,
106 'placeholder': this.options.placeholder,
110 if (this.options.password)
111 frameEl.appendChild(E('button', {
112 'class': 'cbi-button cbi-button-neutral',
113 'title': _('Reveal/hide password'),
114 'aria-label': _('Reveal/hide password'),
115 'click': function(ev) {
116 var e = this.previousElementSibling;
117 e.type = (e.type === 'password') ? 'text' : 'password';
122 return this.bind(frameEl);
125 bind: function(frameEl) {
126 var inputEl = frameEl.childNodes[+!!this.options.password];
130 this.setUpdateEvents(inputEl, 'keyup', 'blur');
131 this.setChangeEvents(inputEl, 'change');
133 L.dom.bindClassInstance(frameEl, this);
138 getValue: function() {
139 var inputEl = this.node.childNodes[+!!this.options.password];
140 return inputEl.value;
143 setValue: function(value) {
144 var inputEl = this.node.childNodes[+!!this.options.password];
145 inputEl.value = value;
149 var UICheckbox = UIElement.extend({
150 __init__: function(value, options) {
152 this.options = Object.assign({
159 var frameEl = E('div', {
160 'id': this.options.id,
161 'class': 'cbi-checkbox'
164 if (this.options.hiddenname)
165 frameEl.appendChild(E('input', {
167 'name': this.options.hiddenname,
171 frameEl.appendChild(E('input', {
172 'id': this.options.id ? 'widget.' + this.options.id : null,
173 'name': this.options.name,
175 'value': this.options.value_enabled,
176 'checked': (this.value == this.options.value_enabled) ? '' : null
179 return this.bind(frameEl);
182 bind: function(frameEl) {
185 this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
186 this.setChangeEvents(frameEl.lastElementChild, 'change');
188 L.dom.bindClassInstance(frameEl, this);
193 isChecked: function() {
194 return this.node.lastElementChild.checked;
197 getValue: function() {
198 return this.isChecked()
199 ? this.options.value_enabled
200 : this.options.value_disabled;
203 setValue: function(value) {
204 this.node.lastElementChild.checked = (value == this.options.value_enabled);
208 var UISelect = UIElement.extend({
209 __init__: function(value, choices, options) {
210 if (typeof(choices) != 'object')
213 if (!Array.isArray(value))
214 value = (value != null && value != '') ? [ value ] : [];
216 if (!options.multiple && value.length > 1)
220 this.choices = choices;
221 this.options = Object.assign({
224 orientation: 'horizontal'
227 if (this.choices.hasOwnProperty(''))
228 this.options.optional = true;
232 var frameEl = E('div', { 'id': this.options.id }),
233 keys = Object.keys(this.choices);
235 if (this.options.sort === true)
237 else if (Array.isArray(this.options.sort))
238 keys = this.options.sort;
240 if (this.options.widget == 'select') {
241 frameEl.appendChild(E('select', {
242 'id': this.options.id ? 'widget.' + this.options.id : null,
243 'name': this.options.name,
244 'size': this.options.size,
245 'class': 'cbi-input-select',
246 'multiple': this.options.multiple ? '' : null
249 if (this.options.optional)
250 frameEl.lastChild.appendChild(E('option', {
252 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
253 }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
255 for (var i = 0; i < keys.length; i++) {
256 if (keys[i] == null || keys[i] == '')
259 frameEl.lastChild.appendChild(E('option', {
261 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
262 }, this.choices[keys[i]] || keys[i]));
266 var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
268 for (var i = 0; i < keys.length; i++) {
269 frameEl.appendChild(E('label', {}, [
271 'id': this.options.id ? 'widget.' + this.options.id : null,
272 'name': this.options.id || this.options.name,
273 'type': this.options.multiple ? 'checkbox' : 'radio',
274 'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
276 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
278 this.choices[keys[i]] || keys[i]
281 if (i + 1 == this.options.size)
282 frameEl.appendChild(brEl);
286 return this.bind(frameEl);
289 bind: function(frameEl) {
292 if (this.options.widget == 'select') {
293 this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
294 this.setChangeEvents(frameEl.firstChild, 'change');
297 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
298 for (var i = 0; i < radioEls.length; i++) {
299 this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
300 this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
304 L.dom.bindClassInstance(frameEl, this);
309 getValue: function() {
310 if (this.options.widget == 'select')
311 return this.node.firstChild.value;
313 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
314 for (var i = 0; i < radioEls.length; i++)
315 if (radioEls[i].checked)
316 return radioEls[i].value;
321 setValue: function(value) {
322 if (this.options.widget == 'select') {
326 for (var i = 0; i < this.node.firstChild.options.length; i++)
327 this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
332 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
333 for (var i = 0; i < radioEls.length; i++)
334 radioEls[i].checked = (radioEls[i].value == value);
338 var UIDropdown = UIElement.extend({
339 __init__: function(value, choices, options) {
340 if (typeof(choices) != 'object')
343 if (!Array.isArray(value))
344 this.values = (value != null && value != '') ? [ value ] : [];
348 this.choices = choices;
349 this.options = Object.assign({
351 multiple: Array.isArray(value),
353 select_placeholder: _('-- Please choose --'),
354 custom_placeholder: _('-- custom --'),
358 create_query: '.create-item-input',
359 create_template: 'script[type="item-template"]'
365 'id': this.options.id,
366 'class': 'cbi-dropdown',
367 'multiple': this.options.multiple ? '' : null,
368 'optional': this.options.optional ? '' : null,
371 var keys = Object.keys(this.choices);
373 if (this.options.sort === true)
375 else if (Array.isArray(this.options.sort))
376 keys = this.options.sort;
378 if (this.options.create)
379 for (var i = 0; i < this.values.length; i++)
380 if (!this.choices.hasOwnProperty(this.values[i]))
381 keys.push(this.values[i]);
383 for (var i = 0; i < keys.length; i++)
384 sb.lastElementChild.appendChild(E('li', {
385 'data-value': keys[i],
386 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
387 }, this.choices[keys[i]] || keys[i]));
389 if (this.options.create) {
390 var createEl = E('input', {
392 'class': 'create-item-input',
393 'readonly': this.options.readonly ? '' : null,
394 'maxlength': this.options.maxlength,
395 'placeholder': this.options.custom_placeholder || this.options.placeholder
398 if (this.options.datatype)
399 L.ui.addValidator(createEl, this.options.datatype,
400 true, null, 'blur', 'keyup');
402 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
405 if (this.options.create_markup)
406 sb.appendChild(E('script', { type: 'item-template' },
407 this.options.create_markup));
409 return this.bind(sb);
413 var o = this.options;
415 o.multiple = sb.hasAttribute('multiple');
416 o.optional = sb.hasAttribute('optional');
417 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
418 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
419 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
420 o.create_query = sb.getAttribute('item-create') || o.create_query;
421 o.create_template = sb.getAttribute('item-template') || o.create_template;
423 var ul = sb.querySelector('ul'),
424 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
425 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
426 canary = sb.appendChild(E('div')),
427 create = sb.querySelector(this.options.create_query),
428 ndisplay = this.options.display_items,
431 if (this.options.multiple) {
432 var items = ul.querySelectorAll('li');
434 for (var i = 0; i < items.length; i++) {
435 this.transformItem(sb, items[i]);
437 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
438 items[i].setAttribute('display', n++);
442 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
443 var placeholder = E('li', { placeholder: '' },
444 this.options.select_placeholder || this.options.placeholder);
447 ? ul.insertBefore(placeholder, ul.firstChild)
448 : ul.appendChild(placeholder);
451 var items = ul.querySelectorAll('li'),
452 sel = sb.querySelectorAll('[selected]');
454 sel.forEach(function(s) {
455 s.removeAttribute('selected');
458 var s = sel[0] || items[0];
460 s.setAttribute('selected', '');
461 s.setAttribute('display', n++);
467 this.saveValues(sb, ul);
469 ul.setAttribute('tabindex', -1);
470 sb.setAttribute('tabindex', 0);
473 sb.setAttribute('more', '')
475 sb.removeAttribute('more');
477 if (ndisplay == this.options.display_items)
478 sb.setAttribute('empty', '')
480 sb.removeAttribute('empty');
482 L.dom.content(more, (ndisplay == this.options.display_items)
483 ? (this.options.select_placeholder || this.options.placeholder) : '···');
486 sb.addEventListener('click', this.handleClick.bind(this));
487 sb.addEventListener('keydown', this.handleKeydown.bind(this));
488 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
489 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
491 if ('ontouchstart' in window) {
492 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
493 window.addEventListener('touchstart', this.closeAllDropdowns);
496 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
497 sb.addEventListener('focus', this.handleFocus.bind(this));
499 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
501 window.addEventListener('mouseover', this.setFocus);
502 window.addEventListener('click', this.closeAllDropdowns);
506 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
507 create.addEventListener('focus', this.handleCreateFocus.bind(this));
508 create.addEventListener('blur', this.handleCreateBlur.bind(this));
510 var li = findParent(create, 'li');
512 li.setAttribute('unselectable', '');
513 li.addEventListener('click', this.handleCreateClick.bind(this));
518 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
519 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
521 L.dom.bindClassInstance(sb, this);
526 openDropdown: function(sb) {
527 var st = window.getComputedStyle(sb, null),
528 ul = sb.querySelector('ul'),
529 li = ul.querySelectorAll('li'),
530 fl = findParent(sb, '.cbi-value-field'),
531 sel = ul.querySelector('[selected]'),
532 rect = sb.getBoundingClientRect(),
533 items = Math.min(this.options.dropdown_items, li.length);
535 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
536 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
539 sb.setAttribute('open', '');
541 var pv = ul.cloneNode(true);
542 pv.classList.add('preview');
545 fl.classList.add('cbi-dropdown-open');
547 if ('ontouchstart' in window) {
548 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
549 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
550 scrollFrom = window.pageYOffset,
551 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
554 ul.style.top = sb.offsetHeight + 'px';
555 ul.style.left = -rect.left + 'px';
556 ul.style.right = (rect.right - vpWidth) + 'px';
557 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
558 ul.style.WebkitOverflowScrolling = 'touch';
560 var scrollStep = function(timestamp) {
563 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
566 var duration = Math.max(timestamp - start, 1);
567 if (duration < 100) {
568 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
569 window.requestAnimationFrame(scrollStep);
572 document.body.scrollTop = scrollTo;
576 window.requestAnimationFrame(scrollStep);
579 ul.style.maxHeight = '1px';
580 ul.style.top = ul.style.bottom = '';
582 window.requestAnimationFrame(function() {
583 var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
585 spaceAbove = rect.top,
586 spaceBelow = window.innerHeight - rect.height - rect.top;
588 for (var i = 0; i < (items == -1 ? li.length : items); i++)
589 fullHeight += li[i].getBoundingClientRect().height;
591 if (fullHeight <= spaceBelow) {
592 ul.style.top = rect.height + 'px';
593 ul.style.maxHeight = spaceBelow + 'px';
595 else if (fullHeight <= spaceAbove) {
596 ul.style.bottom = rect.height + 'px';
597 ul.style.maxHeight = spaceAbove + 'px';
599 else if (spaceBelow >= spaceAbove) {
600 ul.style.top = rect.height + 'px';
601 ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
604 ul.style.bottom = rect.height + 'px';
605 ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
608 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
612 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
613 for (var i = 0; i < cboxes.length; i++) {
614 cboxes[i].checked = true;
615 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
618 ul.classList.add('dropdown');
620 sb.insertBefore(pv, ul.nextElementSibling);
622 li.forEach(function(l) {
623 l.setAttribute('tabindex', 0);
626 sb.lastElementChild.setAttribute('tabindex', 0);
628 this.setFocus(sb, sel || li[0], true);
631 closeDropdown: function(sb, no_focus) {
632 if (!sb.hasAttribute('open'))
635 var pv = sb.querySelector('ul.preview'),
636 ul = sb.querySelector('ul.dropdown'),
637 li = ul.querySelectorAll('li'),
638 fl = findParent(sb, '.cbi-value-field');
640 li.forEach(function(l) { l.removeAttribute('tabindex'); });
641 sb.lastElementChild.removeAttribute('tabindex');
644 sb.removeAttribute('open');
645 sb.style.width = sb.style.height = '';
647 ul.classList.remove('dropdown');
648 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
651 fl.classList.remove('cbi-dropdown-open');
654 this.setFocus(sb, sb);
656 this.saveValues(sb, ul);
659 toggleItem: function(sb, li, force_state) {
660 if (li.hasAttribute('unselectable'))
663 if (this.options.multiple) {
664 var cbox = li.querySelector('input[type="checkbox"]'),
665 items = li.parentNode.querySelectorAll('li'),
666 label = sb.querySelector('ul.preview'),
667 sel = li.parentNode.querySelectorAll('[selected]').length,
668 more = sb.querySelector('.more'),
669 ndisplay = this.options.display_items,
672 if (li.hasAttribute('selected')) {
673 if (force_state !== true) {
674 if (sel > 1 || this.options.optional) {
675 li.removeAttribute('selected');
676 cbox.checked = cbox.disabled = false;
680 cbox.disabled = true;
685 if (force_state !== false) {
686 li.setAttribute('selected', '');
688 cbox.disabled = false;
693 while (label && label.firstElementChild)
694 label.removeChild(label.firstElementChild);
696 for (var i = 0; i < items.length; i++) {
697 items[i].removeAttribute('display');
698 if (items[i].hasAttribute('selected')) {
699 if (ndisplay-- > 0) {
700 items[i].setAttribute('display', n++);
702 label.appendChild(items[i].cloneNode(true));
704 var c = items[i].querySelector('input[type="checkbox"]');
706 c.disabled = (sel == 1 && !this.options.optional);
711 sb.setAttribute('more', '');
713 sb.removeAttribute('more');
715 if (ndisplay === this.options.display_items)
716 sb.setAttribute('empty', '');
718 sb.removeAttribute('empty');
720 L.dom.content(more, (ndisplay === this.options.display_items)
721 ? (this.options.select_placeholder || this.options.placeholder) : '···');
724 var sel = li.parentNode.querySelector('[selected]');
726 sel.removeAttribute('display');
727 sel.removeAttribute('selected');
730 li.setAttribute('display', 0);
731 li.setAttribute('selected', '');
733 this.closeDropdown(sb, true);
736 this.saveValues(sb, li.parentNode);
739 transformItem: function(sb, li) {
740 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
743 while (li.firstChild)
744 label.appendChild(li.firstChild);
746 li.appendChild(cbox);
747 li.appendChild(label);
750 saveValues: function(sb, ul) {
751 var sel = ul.querySelectorAll('li[selected]'),
752 div = sb.lastElementChild,
753 name = this.options.name,
757 while (div.lastElementChild)
758 div.removeChild(div.lastElementChild);
760 sel.forEach(function (s) {
761 if (s.hasAttribute('placeholder'))
766 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
770 div.appendChild(E('input', {
778 strval += strval.length ? ' ' + v.value : v.value;
786 if (this.options.multiple)
787 detail.values = values;
789 detail.value = values.length ? values[0] : null;
793 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
799 setValues: function(sb, values) {
800 var ul = sb.querySelector('ul');
802 if (this.options.create) {
803 for (var value in values) {
804 this.createItems(sb, value);
806 if (!this.options.multiple)
811 if (this.options.multiple) {
812 var lis = ul.querySelectorAll('li[data-value]');
813 for (var i = 0; i < lis.length; i++) {
814 var value = lis[i].getAttribute('data-value');
815 if (values === null || !(value in values))
816 this.toggleItem(sb, lis[i], false);
818 this.toggleItem(sb, lis[i], true);
822 var ph = ul.querySelector('li[placeholder]');
824 this.toggleItem(sb, ph);
826 var lis = ul.querySelectorAll('li[data-value]');
827 for (var i = 0; i < lis.length; i++) {
828 var value = lis[i].getAttribute('data-value');
829 if (values !== null && (value in values))
830 this.toggleItem(sb, lis[i]);
835 setFocus: function(sb, elem, scroll) {
836 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
839 if (sb.target && findParent(sb.target, 'ul.dropdown'))
842 document.querySelectorAll('.focus').forEach(function(e) {
843 if (!matchesElem(e, 'input')) {
844 e.classList.remove('focus');
851 elem.classList.add('focus');
854 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
858 createItems: function(sb, value) {
860 val = (value || '').trim(),
861 ul = sb.querySelector('ul');
863 if (!sbox.options.multiple)
864 val = val.length ? [ val ] : [];
866 val = val.length ? val.split(/\s+/) : [];
868 val.forEach(function(item) {
871 ul.childNodes.forEach(function(li) {
872 if (li.getAttribute && li.getAttribute('data-value') === item)
878 tpl = sb.querySelector(sbox.options.create_template);
881 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
883 markup = '<li data-value="{{value}}">{{value}}</li>';
885 new_item = E(markup.replace(/{{value}}/g, '%h'.format(item)));
887 if (sbox.options.multiple) {
888 sbox.transformItem(sb, new_item);
891 var old = ul.querySelector('li[created]');
895 new_item.setAttribute('created', '');
898 new_item = ul.insertBefore(new_item, ul.lastElementChild);
901 sbox.toggleItem(sb, new_item, true);
902 sbox.setFocus(sb, new_item, true);
906 closeAllDropdowns: function() {
907 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
908 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
912 handleClick: function(ev) {
913 var sb = ev.currentTarget;
915 if (!sb.hasAttribute('open')) {
916 if (!matchesElem(ev.target, 'input'))
917 this.openDropdown(sb);
920 var li = findParent(ev.target, 'li');
921 if (li && li.parentNode.classList.contains('dropdown'))
922 this.toggleItem(sb, li);
923 else if (li && li.parentNode.classList.contains('preview'))
924 this.closeDropdown(sb);
925 else if (matchesElem(ev.target, 'span.open, span.more'))
926 this.closeDropdown(sb);
930 ev.stopPropagation();
933 handleKeydown: function(ev) {
934 var sb = ev.currentTarget;
936 if (matchesElem(ev.target, 'input'))
939 if (!sb.hasAttribute('open')) {
940 switch (ev.keyCode) {
945 this.openDropdown(sb);
950 var active = findParent(document.activeElement, 'li');
952 switch (ev.keyCode) {
954 this.closeDropdown(sb);
959 if (!active.hasAttribute('selected'))
960 this.toggleItem(sb, active);
961 this.closeDropdown(sb);
968 this.toggleItem(sb, active);
974 if (active && active.previousElementSibling) {
975 this.setFocus(sb, active.previousElementSibling);
981 if (active && active.nextElementSibling) {
982 this.setFocus(sb, active.nextElementSibling);
990 handleDropdownClose: function(ev) {
991 var sb = ev.currentTarget;
993 this.closeDropdown(sb, true);
996 handleDropdownSelect: function(ev) {
997 var sb = ev.currentTarget,
998 li = findParent(ev.target, 'li');
1003 this.toggleItem(sb, li);
1004 this.closeDropdown(sb, true);
1007 handleMouseover: function(ev) {
1008 var sb = ev.currentTarget;
1010 if (!sb.hasAttribute('open'))
1013 var li = findParent(ev.target, 'li');
1015 if (li && li.parentNode.classList.contains('dropdown'))
1016 this.setFocus(sb, li);
1019 handleFocus: function(ev) {
1020 var sb = ev.currentTarget;
1022 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
1023 if (s !== sb || sb.hasAttribute('open'))
1024 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1028 handleCanaryFocus: function(ev) {
1029 this.closeDropdown(ev.currentTarget.parentNode);
1032 handleCreateKeydown: function(ev) {
1033 var input = ev.currentTarget,
1034 sb = findParent(input, '.cbi-dropdown');
1036 switch (ev.keyCode) {
1038 ev.preventDefault();
1040 if (input.classList.contains('cbi-input-invalid'))
1043 this.createItems(sb, input.value);
1050 handleCreateFocus: function(ev) {
1051 var input = ev.currentTarget,
1052 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1053 sb = findParent(input, '.cbi-dropdown');
1056 cbox.checked = true;
1058 sb.setAttribute('locked-in', '');
1061 handleCreateBlur: function(ev) {
1062 var input = ev.currentTarget,
1063 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1064 sb = findParent(input, '.cbi-dropdown');
1067 cbox.checked = false;
1069 sb.removeAttribute('locked-in');
1072 handleCreateClick: function(ev) {
1073 ev.currentTarget.querySelector(this.options.create_query).focus();
1076 setValue: function(values) {
1077 if (this.options.multiple) {
1078 if (!Array.isArray(values))
1079 values = (values != null && values != '') ? [ values ] : [];
1083 for (var i = 0; i < values.length; i++)
1084 v[values[i]] = true;
1086 this.setValues(this.node, v);
1091 if (values != null) {
1092 if (Array.isArray(values))
1093 v[values[0]] = true;
1098 this.setValues(this.node, v);
1102 getValue: function() {
1103 var div = this.node.lastElementChild,
1104 h = div.querySelectorAll('input[type="hidden"]'),
1107 for (var i = 0; i < h.length; i++)
1110 return this.options.multiple ? v : v[0];
1114 var UICombobox = UIDropdown.extend({
1115 __init__: function(value, choices, options) {
1116 this.super('__init__', [ value, choices, Object.assign({
1117 select_placeholder: _('-- Please choose --'),
1118 custom_placeholder: _('-- custom --'),
1129 var UIDynamicList = UIElement.extend({
1130 __init__: function(values, choices, options) {
1131 if (!Array.isArray(values))
1132 values = (values != null && values != '') ? [ values ] : [];
1134 if (typeof(choices) != 'object')
1137 this.values = values;
1138 this.choices = choices;
1139 this.options = Object.assign({}, options, {
1145 render: function() {
1147 'id': this.options.id,
1148 'class': 'cbi-dynlist'
1149 }, E('div', { 'class': 'add-item' }));
1152 var cbox = new UICombobox(null, this.choices, this.options);
1153 dl.lastElementChild.appendChild(cbox.render());
1156 var inputEl = E('input', {
1157 'id': this.options.id ? 'widget.' + this.options.id : null,
1159 'class': 'cbi-input-text',
1160 'placeholder': this.options.placeholder
1163 dl.lastElementChild.appendChild(inputEl);
1164 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1166 if (this.options.datatype)
1167 L.ui.addValidator(inputEl, this.options.datatype,
1168 true, null, 'blur', 'keyup');
1171 for (var i = 0; i < this.values.length; i++)
1172 this.addItem(dl, this.values[i],
1173 this.choices ? this.choices[this.values[i]] : null);
1175 return this.bind(dl);
1178 bind: function(dl) {
1179 dl.addEventListener('click', L.bind(this.handleClick, this));
1180 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
1181 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
1185 this.setUpdateEvents(dl, 'cbi-dynlist-change');
1186 this.setChangeEvents(dl, 'cbi-dynlist-change');
1188 L.dom.bindClassInstance(dl, this);
1193 addItem: function(dl, value, text, flash) {
1195 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
1196 E('span', {}, text || value),
1199 'name': this.options.name,
1200 'value': value })]);
1202 dl.querySelectorAll('.item, .add-item').forEach(function(item) {
1206 var hidden = item.querySelector('input[type="hidden"]');
1208 if (hidden && hidden.parentNode !== item)
1211 if (hidden && hidden.value === value)
1213 else if (!hidden || hidden.value >= value)
1214 exists = !!item.parentNode.insertBefore(new_item, item);
1217 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1228 removeItem: function(dl, item) {
1229 var value = item.querySelector('input[type="hidden"]').value;
1230 var sb = dl.querySelector('.cbi-dropdown');
1232 sb.querySelectorAll('ul > li').forEach(function(li) {
1233 if (li.getAttribute('data-value') === value) {
1234 if (li.hasAttribute('dynlistcustom'))
1235 li.parentNode.removeChild(li);
1237 li.removeAttribute('unselectable');
1241 item.parentNode.removeChild(item);
1243 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1254 handleClick: function(ev) {
1255 var dl = ev.currentTarget,
1256 item = findParent(ev.target, '.item');
1259 this.removeItem(dl, item);
1261 else if (matchesElem(ev.target, '.cbi-button-add')) {
1262 var input = ev.target.previousElementSibling;
1263 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
1264 this.addItem(dl, input.value, null, true);
1270 handleDropdownChange: function(ev) {
1271 var dl = ev.currentTarget,
1272 sbIn = ev.detail.instance,
1273 sbEl = ev.detail.element,
1274 sbVal = ev.detail.value;
1279 sbIn.setValues(sbEl, null);
1280 sbVal.element.setAttribute('unselectable', '');
1282 if (sbVal.element.hasAttribute('created')) {
1283 sbVal.element.removeAttribute('created');
1284 sbVal.element.setAttribute('dynlistcustom', '');
1287 this.addItem(dl, sbVal.value, sbVal.text, true);
1290 handleKeydown: function(ev) {
1291 var dl = ev.currentTarget,
1292 item = findParent(ev.target, '.item');
1295 switch (ev.keyCode) {
1296 case 8: /* backspace */
1297 if (item.previousElementSibling)
1298 item.previousElementSibling.focus();
1300 this.removeItem(dl, item);
1303 case 46: /* delete */
1304 if (item.nextElementSibling) {
1305 if (item.nextElementSibling.classList.contains('item'))
1306 item.nextElementSibling.focus();
1308 item.nextElementSibling.firstElementChild.focus();
1311 this.removeItem(dl, item);
1315 else if (matchesElem(ev.target, '.cbi-input-text')) {
1316 switch (ev.keyCode) {
1317 case 13: /* enter */
1318 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
1319 this.addItem(dl, ev.target.value, null, true);
1320 ev.target.value = '';
1325 ev.preventDefault();
1331 getValue: function() {
1332 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1333 input = this.node.querySelector('.add-item > input[type="text"]'),
1336 for (var i = 0; i < items.length; i++)
1337 v.push(items[i].value);
1339 if (input && input.value != null && input.value.match(/\S/) &&
1340 input.classList.contains('cbi-input-invalid') == false &&
1341 v.filter(function(s) { return s == input.value }).length == 0)
1342 v.push(input.value);
1347 setValue: function(values) {
1348 if (!Array.isArray(values))
1349 values = (values != null && values != '') ? [ values ] : [];
1351 var items = this.node.querySelectorAll('.item');
1353 for (var i = 0; i < items.length; i++)
1354 if (items[i].parentNode === this.node)
1355 this.removeItem(this.node, items[i]);
1357 for (var i = 0; i < values.length; i++)
1358 this.addItem(this.node, values[i],
1359 this.choices ? this.choices[values[i]] : null);
1363 var UIHiddenfield = UIElement.extend({
1364 __init__: function(value, options) {
1366 this.options = Object.assign({
1371 render: function() {
1372 var hiddenEl = E('input', {
1373 'id': this.options.id,
1378 return this.bind(hiddenEl);
1381 bind: function(hiddenEl) {
1382 this.node = hiddenEl;
1384 L.dom.bindClassInstance(hiddenEl, this);
1389 getValue: function() {
1390 return this.node.value;
1393 setValue: function(value) {
1394 this.node.value = value;
1399 return L.Class.extend({
1400 __init__: function() {
1401 modalDiv = document.body.appendChild(
1402 L.dom.create('div', { id: 'modal_overlay' },
1403 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1405 tooltipDiv = document.body.appendChild(
1406 L.dom.create('div', { class: 'cbi-tooltip' }));
1408 /* setup old aliases */
1409 L.showModal = this.showModal;
1410 L.hideModal = this.hideModal;
1411 L.showTooltip = this.showTooltip;
1412 L.hideTooltip = this.hideTooltip;
1413 L.itemlist = this.itemlist;
1415 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1416 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1417 document.addEventListener('focus', this.showTooltip.bind(this), true);
1418 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1420 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1421 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
1422 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
1426 showModal: function(title, children /* , ... */) {
1427 var dlg = modalDiv.firstElementChild;
1429 dlg.setAttribute('class', 'modal');
1431 for (var i = 2; i < arguments.length; i++)
1432 dlg.classList.add(arguments[i]);
1434 L.dom.content(dlg, L.dom.create('h4', {}, title));
1435 L.dom.append(dlg, children);
1437 document.body.classList.add('modal-overlay-active');
1442 hideModal: function() {
1443 document.body.classList.remove('modal-overlay-active');
1447 showTooltip: function(ev) {
1448 var target = findParent(ev.target, '[data-tooltip]');
1453 if (tooltipTimeout !== null) {
1454 window.clearTimeout(tooltipTimeout);
1455 tooltipTimeout = null;
1458 var rect = target.getBoundingClientRect(),
1459 x = rect.left + window.pageXOffset,
1460 y = rect.top + rect.height + window.pageYOffset;
1462 tooltipDiv.className = 'cbi-tooltip';
1463 tooltipDiv.innerHTML = '▲ ';
1464 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1466 if (target.hasAttribute('data-tooltip-style'))
1467 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1469 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1470 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1471 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1474 tooltipDiv.style.top = y + 'px';
1475 tooltipDiv.style.left = x + 'px';
1476 tooltipDiv.style.opacity = 1;
1478 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1480 detail: { target: target }
1484 hideTooltip: function(ev) {
1485 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1486 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1489 if (tooltipTimeout !== null) {
1490 window.clearTimeout(tooltipTimeout);
1491 tooltipTimeout = null;
1494 tooltipDiv.style.opacity = 0;
1495 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1497 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1501 itemlist: function(node, items, separators) {
1504 if (!Array.isArray(separators))
1505 separators = [ separators || E('br') ];
1507 for (var i = 0; i < items.length; i += 2) {
1508 if (items[i+1] !== null && items[i+1] !== undefined) {
1509 var sep = separators[(i/2) % separators.length],
1512 children.push(E('span', { class: 'nowrap' }, [
1513 items[i] ? E('strong', items[i] + ': ') : '',
1517 if ((i+2) < items.length)
1518 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
1522 L.dom.content(node, children);
1528 tabs: L.Class.singleton({
1530 var groups = [], prevGroup = null, currGroup = null;
1532 document.querySelectorAll('[data-tab]').forEach(function(tab) {
1533 var parent = tab.parentNode;
1535 if (!parent.hasAttribute('data-tab-group'))
1536 parent.setAttribute('data-tab-group', groups.length);
1538 currGroup = +parent.getAttribute('data-tab-group');
1540 if (currGroup !== prevGroup) {
1541 prevGroup = currGroup;
1543 if (!groups[currGroup])
1544 groups[currGroup] = [];
1547 groups[currGroup].push(tab);
1550 for (var i = 0; i < groups.length; i++)
1551 this.initTabGroup(groups[i]);
1553 document.addEventListener('dependency-update', this.updateTabs.bind(this));
1558 this.setActiveTabId(-1, -1);
1561 initTabGroup: function(panes) {
1562 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
1565 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
1566 group = panes[0].parentNode,
1567 groupId = +group.getAttribute('data-tab-group'),
1570 for (var i = 0, pane; pane = panes[i]; i++) {
1571 var name = pane.getAttribute('data-tab'),
1572 title = pane.getAttribute('data-tab-title'),
1573 active = pane.getAttribute('data-tab-active') === 'true';
1575 menu.appendChild(E('li', {
1576 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
1580 'click': this.switchTab.bind(this)
1587 group.parentNode.insertBefore(menu, group);
1589 if (selected === null) {
1590 selected = this.getActiveTabId(groupId);
1592 if (selected < 0 || selected >= panes.length || L.dom.isEmpty(panes[selected])) {
1593 for (var i = 0; i < panes.length; i++) {
1594 if (!L.dom.isEmpty(panes[i])) {
1601 menu.childNodes[selected].classList.add('cbi-tab');
1602 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
1603 panes[selected].setAttribute('data-tab-active', 'true');
1605 this.setActiveTabId(groupId, selected);
1609 getActiveTabState: function() {
1610 var page = document.body.getAttribute('data-page');
1613 var val = JSON.parse(window.sessionStorage.getItem('tab'));
1614 if (val.page === page && Array.isArray(val.groups))
1619 window.sessionStorage.removeItem('tab');
1620 return { page: page, groups: [] };
1623 getActiveTabId: function(groupId) {
1624 return +this.getActiveTabState().groups[groupId] || 0;
1627 setActiveTabId: function(groupId, tabIndex) {
1629 var state = this.getActiveTabState();
1630 state.groups[groupId] = tabIndex;
1632 window.sessionStorage.setItem('tab', JSON.stringify(state));
1634 catch (e) { return false; }
1639 updateTabs: function(ev, root) {
1640 (root || document).querySelectorAll('[data-tab-title]').forEach(function(pane) {
1641 var menu = pane.parentNode.previousElementSibling,
1642 tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
1643 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
1645 if (L.dom.isEmpty(pane)) {
1646 tab.style.display = 'none';
1647 tab.classList.remove('flash');
1649 else if (tab.style.display === 'none') {
1650 tab.style.display = '';
1651 requestAnimationFrame(function() { tab.classList.add('flash') });
1655 tab.setAttribute('data-errors', n_errors);
1656 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
1657 tab.setAttribute('data-tooltip-style', 'error');
1660 tab.removeAttribute('data-errors');
1661 tab.removeAttribute('data-tooltip');
1666 switchTab: function(ev) {
1667 var tab = ev.target.parentNode,
1668 name = tab.getAttribute('data-tab'),
1669 menu = tab.parentNode,
1670 group = menu.nextElementSibling,
1671 groupId = +group.getAttribute('data-tab-group'),
1674 ev.preventDefault();
1676 if (!tab.classList.contains('cbi-tab-disabled'))
1679 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
1680 tab.classList.remove('cbi-tab');
1681 tab.classList.remove('cbi-tab-disabled');
1683 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
1686 group.childNodes.forEach(function(pane) {
1687 if (L.dom.matches(pane, '[data-tab]')) {
1688 if (pane.getAttribute('data-tab') === name) {
1689 pane.setAttribute('data-tab-active', 'true');
1690 L.ui.tabs.setActiveTabId(groupId, index);
1693 pane.setAttribute('data-tab-active', 'false');
1703 changes: L.Class.singleton({
1705 if (!L.env.sessionid)
1708 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
1711 setIndicator: function(n) {
1712 var i = document.querySelector('.uci_change_indicator');
1714 var poll = document.getElementById('xhr_poll_status');
1715 i = poll.parentNode.insertBefore(E('a', {
1717 'class': 'uci_change_indicator label notice',
1718 'click': L.bind(this.displayChanges, this)
1723 L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
1724 i.classList.add('flash');
1725 i.style.display = '';
1728 i.classList.remove('flash');
1729 i.style.display = 'none';
1733 renderChangeIndicator: function(changes) {
1736 for (var config in changes)
1737 if (changes.hasOwnProperty(config))
1738 n_changes += changes[config].length;
1740 this.changes = changes;
1741 this.setIndicator(n_changes);
1745 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1746 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1747 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1748 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1749 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1750 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1751 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1752 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1753 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1754 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1757 displayChanges: function() {
1758 var list = E('div', { 'class': 'uci-change-list' }),
1759 dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
1760 E('div', { 'class': 'cbi-section' }, [
1761 E('strong', _('Legend:')),
1762 E('div', { 'class': 'uci-change-legend' }, [
1763 E('div', { 'class': 'uci-change-legend-label' }, [
1764 E('ins', ' '), ' ', _('Section added') ]),
1765 E('div', { 'class': 'uci-change-legend-label' }, [
1766 E('del', ' '), ' ', _('Section removed') ]),
1767 E('div', { 'class': 'uci-change-legend-label' }, [
1768 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1769 E('div', { 'class': 'uci-change-legend-label' }, [
1770 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1772 E('div', { 'class': 'right' }, [
1776 'click': L.ui.hideModal,
1777 'value': _('Dismiss')
1781 'class': 'cbi-button cbi-button-positive important',
1782 'click': L.bind(this.apply, this, true),
1783 'value': _('Save & Apply')
1787 'class': 'cbi-button cbi-button-reset',
1788 'click': L.bind(this.revert, this),
1789 'value': _('Revert')
1793 for (var config in this.changes) {
1794 if (!this.changes.hasOwnProperty(config))
1797 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
1799 for (var i = 0, added = null; i < this.changes[config].length; i++) {
1800 var chg = this.changes[config][i],
1801 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
1803 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
1809 if (added != null && chg[1] == added[0])
1810 return '@' + added[1] + '[-1]';
1815 return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
1822 if (chg[0] == 'add')
1823 added = [ chg[1], chg[2] ];
1827 list.appendChild(E('br'));
1828 dlg.classList.add('uci-dialog');
1831 displayStatus: function(type, content) {
1833 var message = L.ui.showModal('', '');
1835 message.classList.add('alert-message');
1836 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1839 L.dom.content(message, content);
1841 if (!this.was_polling) {
1842 this.was_polling = L.Request.poll.active();
1843 L.Request.poll.stop();
1849 if (this.was_polling)
1850 L.Request.poll.start();
1854 rollback: function(checked) {
1856 this.displayStatus('warning spinning',
1857 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
1858 .format(L.env.apply_rollback)));
1860 var call = function(r, data, duration) {
1861 if (r.status === 204) {
1862 L.ui.changes.displayStatus('warning', [
1863 E('h4', _('Configuration has been rolled back!')),
1864 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)),
1865 E('div', { 'class': 'right' }, [
1869 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1870 'value': _('Dismiss')
1874 'class': 'btn cbi-button-action important',
1875 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1876 'value': _('Revert changes')
1880 'class': 'btn cbi-button-negative important',
1881 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1882 'value': _('Apply unchecked')
1890 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1891 window.setTimeout(function() {
1892 L.Request.request(L.url('admin/uci/confirm'), {
1894 timeout: L.env.apply_timeout * 1000,
1895 query: { sid: L.env.sessionid, token: L.env.token }
1900 call({ status: 0 });
1903 this.displayStatus('warning', [
1904 E('h4', _('Device unreachable!')),
1905 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.'))
1910 confirm: function(checked, deadline, override_token) {
1912 var ts = Date.now();
1914 this.displayStatus('notice');
1917 this.confirm_auth = { token: override_token };
1919 var call = function(r, data, duration) {
1920 if (Date.now() >= deadline) {
1921 window.clearTimeout(tt);
1922 L.ui.changes.rollback(checked);
1925 else if (r && (r.status === 200 || r.status === 204)) {
1926 document.dispatchEvent(new CustomEvent('uci-applied'));
1928 L.ui.changes.setIndicator(0);
1929 L.ui.changes.displayStatus('notice',
1930 E('p', _('Configuration has been applied.')));
1932 window.clearTimeout(tt);
1933 window.setTimeout(function() {
1934 //L.ui.changes.displayStatus(false);
1935 window.location = window.location.href.split('#')[0];
1936 }, L.env.apply_display * 1000);
1941 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1942 window.setTimeout(function() {
1943 L.Request.request(L.url('admin/uci/confirm'), {
1945 timeout: L.env.apply_timeout * 1000,
1946 query: L.ui.changes.confirm_auth
1947 }).then(call, call);
1951 var tick = function() {
1952 var now = Date.now();
1954 L.ui.changes.displayStatus('notice spinning',
1955 E('p', _('Waiting for configuration to get applied… %ds')
1956 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
1958 if (now >= deadline)
1961 tt = window.setTimeout(tick, 1000 - (now - ts));
1967 /* wait a few seconds for the settings to become effective */
1968 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
1971 apply: function(checked) {
1972 this.displayStatus('notice spinning',
1973 E('p', _('Starting configuration apply…')));
1975 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
1977 query: { sid: L.env.sessionid, token: L.env.token }
1978 }).then(function(r) {
1979 if (r.status === (checked ? 200 : 204)) {
1980 var tok = null; try { tok = r.json(); } catch(e) {}
1981 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
1982 L.ui.changes.confirm_auth = tok;
1984 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
1986 else if (checked && r.status === 204) {
1987 L.ui.changes.displayStatus('notice',
1988 E('p', _('There are no changes to apply')));
1990 window.setTimeout(function() {
1991 L.ui.changes.displayStatus(false);
1992 }, L.env.apply_display * 1000);
1995 L.ui.changes.displayStatus('warning',
1996 E('p', _('Apply request failed with status <code>%h</code>')
1997 .format(r.responseText || r.statusText || r.status)));
1999 window.setTimeout(function() {
2000 L.ui.changes.displayStatus(false);
2001 }, L.env.apply_display * 1000);
2006 revert: function() {
2007 this.displayStatus('notice spinning',
2008 E('p', _('Reverting configuration…')));
2010 L.Request.request(L.url('admin/uci/revert'), {
2012 query: { sid: L.env.sessionid, token: L.env.token }
2013 }).then(function(r) {
2014 if (r.status === 200) {
2015 document.dispatchEvent(new CustomEvent('uci-reverted'));
2017 L.ui.changes.setIndicator(0);
2018 L.ui.changes.displayStatus('notice',
2019 E('p', _('Changes have been reverted.')));
2021 window.setTimeout(function() {
2022 //L.ui.changes.displayStatus(false);
2023 window.location = window.location.href.split('#')[0];
2024 }, L.env.apply_display * 1000);
2027 L.ui.changes.displayStatus('warning',
2028 E('p', _('Revert request failed with status <code>%h</code>')
2029 .format(r.statusText || r.status)));
2031 window.setTimeout(function() {
2032 L.ui.changes.displayStatus(false);
2033 }, L.env.apply_display * 1000);
2039 addValidator: function(field, type, optional, vfunc /*, ... */) {
2043 var events = this.varargs(arguments, 3);
2044 if (events.length == 0)
2045 events.push('blur', 'keyup');
2048 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2049 validatorFn = cbiValidator.validate.bind(cbiValidator);
2051 for (var i = 0; i < events.length; i++)
2052 field.addEventListener(events[i], validatorFn);
2062 Textfield: UITextfield,
2063 Checkbox: UICheckbox,
2065 Dropdown: UIDropdown,
2066 DynamicList: UIDynamicList,
2067 Combobox: UICombobox,
2068 Hiddenfield: UIHiddenfield