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 UITextarea = UIElement.extend({
150 __init__: function(value, options) {
152 this.options = Object.assign({
161 var frameEl = E('div', { 'id': this.options.id }),
162 value = (this.value != null) ? String(this.value) : '';
164 frameEl.appendChild(E('textarea', {
165 'id': this.options.id ? 'widget.' + this.options.id : null,
166 'name': this.options.name,
167 'class': 'cbi-input-textarea',
168 'readonly': this.options.readonly ? '' : null,
169 'placeholder': this.options.placeholder,
170 'style': !this.options.cols ? 'width:100%' : null,
171 'cols': this.options.cols,
172 'rows': this.options.rows,
173 'wrap': this.options.wrap ? '' : null
176 if (this.options.monospace)
177 frameEl.firstElementChild.style.fontFamily = 'monospace';
179 return this.bind(frameEl);
182 bind: function(frameEl) {
183 var inputEl = frameEl.firstElementChild;
187 this.setUpdateEvents(inputEl, 'keyup', 'blur');
188 this.setChangeEvents(inputEl, 'change');
190 L.dom.bindClassInstance(frameEl, this);
195 getValue: function() {
196 return this.node.firstElementChild.value;
199 setValue: function(value) {
200 this.node.firstElementChild.value = value;
204 var UICheckbox = UIElement.extend({
205 __init__: function(value, options) {
207 this.options = Object.assign({
214 var frameEl = E('div', {
215 'id': this.options.id,
216 'class': 'cbi-checkbox'
219 if (this.options.hiddenname)
220 frameEl.appendChild(E('input', {
222 'name': this.options.hiddenname,
226 frameEl.appendChild(E('input', {
227 'id': this.options.id ? 'widget.' + this.options.id : null,
228 'name': this.options.name,
230 'value': this.options.value_enabled,
231 'checked': (this.value == this.options.value_enabled) ? '' : null
234 return this.bind(frameEl);
237 bind: function(frameEl) {
240 this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
241 this.setChangeEvents(frameEl.lastElementChild, 'change');
243 L.dom.bindClassInstance(frameEl, this);
248 isChecked: function() {
249 return this.node.lastElementChild.checked;
252 getValue: function() {
253 return this.isChecked()
254 ? this.options.value_enabled
255 : this.options.value_disabled;
258 setValue: function(value) {
259 this.node.lastElementChild.checked = (value == this.options.value_enabled);
263 var UISelect = UIElement.extend({
264 __init__: function(value, choices, options) {
265 if (!L.isObject(choices))
268 if (!Array.isArray(value))
269 value = (value != null && value != '') ? [ value ] : [];
271 if (!options.multiple && value.length > 1)
275 this.choices = choices;
276 this.options = Object.assign({
279 orientation: 'horizontal'
282 if (this.choices.hasOwnProperty(''))
283 this.options.optional = true;
287 var frameEl = E('div', { 'id': this.options.id }),
288 keys = Object.keys(this.choices);
290 if (this.options.sort === true)
292 else if (Array.isArray(this.options.sort))
293 keys = this.options.sort;
295 if (this.options.widget == 'select') {
296 frameEl.appendChild(E('select', {
297 'id': this.options.id ? 'widget.' + this.options.id : null,
298 'name': this.options.name,
299 'size': this.options.size,
300 'class': 'cbi-input-select',
301 'multiple': this.options.multiple ? '' : null
304 if (this.options.optional)
305 frameEl.lastChild.appendChild(E('option', {
307 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
308 }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
310 for (var i = 0; i < keys.length; i++) {
311 if (keys[i] == null || keys[i] == '')
314 frameEl.lastChild.appendChild(E('option', {
316 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
317 }, this.choices[keys[i]] || keys[i]));
321 var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
323 for (var i = 0; i < keys.length; i++) {
324 frameEl.appendChild(E('label', {}, [
326 'id': this.options.id ? 'widget.' + this.options.id : null,
327 'name': this.options.id || this.options.name,
328 'type': this.options.multiple ? 'checkbox' : 'radio',
329 'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
331 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
333 this.choices[keys[i]] || keys[i]
336 if (i + 1 == this.options.size)
337 frameEl.appendChild(brEl);
341 return this.bind(frameEl);
344 bind: function(frameEl) {
347 if (this.options.widget == 'select') {
348 this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
349 this.setChangeEvents(frameEl.firstChild, 'change');
352 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
353 for (var i = 0; i < radioEls.length; i++) {
354 this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
355 this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
359 L.dom.bindClassInstance(frameEl, this);
364 getValue: function() {
365 if (this.options.widget == 'select')
366 return this.node.firstChild.value;
368 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
369 for (var i = 0; i < radioEls.length; i++)
370 if (radioEls[i].checked)
371 return radioEls[i].value;
376 setValue: function(value) {
377 if (this.options.widget == 'select') {
381 for (var i = 0; i < this.node.firstChild.options.length; i++)
382 this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
387 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
388 for (var i = 0; i < radioEls.length; i++)
389 radioEls[i].checked = (radioEls[i].value == value);
393 var UIDropdown = UIElement.extend({
394 __init__: function(value, choices, options) {
395 if (typeof(choices) != 'object')
398 if (!Array.isArray(value))
399 this.values = (value != null && value != '') ? [ value ] : [];
403 this.choices = choices;
404 this.options = Object.assign({
406 multiple: Array.isArray(value),
408 select_placeholder: _('-- Please choose --'),
409 custom_placeholder: _('-- custom --'),
413 create_query: '.create-item-input',
414 create_template: 'script[type="item-template"]'
420 'id': this.options.id,
421 'class': 'cbi-dropdown',
422 'multiple': this.options.multiple ? '' : null,
423 'optional': this.options.optional ? '' : null,
426 var keys = Object.keys(this.choices);
428 if (this.options.sort === true)
430 else if (Array.isArray(this.options.sort))
431 keys = this.options.sort;
433 if (this.options.create)
434 for (var i = 0; i < this.values.length; i++)
435 if (!this.choices.hasOwnProperty(this.values[i]))
436 keys.push(this.values[i]);
438 for (var i = 0; i < keys.length; i++)
439 sb.lastElementChild.appendChild(E('li', {
440 'data-value': keys[i],
441 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
442 }, this.choices[keys[i]] || keys[i]));
444 if (this.options.create) {
445 var createEl = E('input', {
447 'class': 'create-item-input',
448 'readonly': this.options.readonly ? '' : null,
449 'maxlength': this.options.maxlength,
450 'placeholder': this.options.custom_placeholder || this.options.placeholder
453 if (this.options.datatype)
454 L.ui.addValidator(createEl, this.options.datatype,
455 true, null, 'blur', 'keyup');
457 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
460 if (this.options.create_markup)
461 sb.appendChild(E('script', { type: 'item-template' },
462 this.options.create_markup));
464 return this.bind(sb);
468 var o = this.options;
470 o.multiple = sb.hasAttribute('multiple');
471 o.optional = sb.hasAttribute('optional');
472 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
473 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
474 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
475 o.create_query = sb.getAttribute('item-create') || o.create_query;
476 o.create_template = sb.getAttribute('item-template') || o.create_template;
478 var ul = sb.querySelector('ul'),
479 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
480 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
481 canary = sb.appendChild(E('div')),
482 create = sb.querySelector(this.options.create_query),
483 ndisplay = this.options.display_items,
486 if (this.options.multiple) {
487 var items = ul.querySelectorAll('li');
489 for (var i = 0; i < items.length; i++) {
490 this.transformItem(sb, items[i]);
492 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
493 items[i].setAttribute('display', n++);
497 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
498 var placeholder = E('li', { placeholder: '' },
499 this.options.select_placeholder || this.options.placeholder);
502 ? ul.insertBefore(placeholder, ul.firstChild)
503 : ul.appendChild(placeholder);
506 var items = ul.querySelectorAll('li'),
507 sel = sb.querySelectorAll('[selected]');
509 sel.forEach(function(s) {
510 s.removeAttribute('selected');
513 var s = sel[0] || items[0];
515 s.setAttribute('selected', '');
516 s.setAttribute('display', n++);
522 this.saveValues(sb, ul);
524 ul.setAttribute('tabindex', -1);
525 sb.setAttribute('tabindex', 0);
528 sb.setAttribute('more', '')
530 sb.removeAttribute('more');
532 if (ndisplay == this.options.display_items)
533 sb.setAttribute('empty', '')
535 sb.removeAttribute('empty');
537 L.dom.content(more, (ndisplay == this.options.display_items)
538 ? (this.options.select_placeholder || this.options.placeholder) : '···');
541 sb.addEventListener('click', this.handleClick.bind(this));
542 sb.addEventListener('keydown', this.handleKeydown.bind(this));
543 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
544 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
546 if ('ontouchstart' in window) {
547 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
548 window.addEventListener('touchstart', this.closeAllDropdowns);
551 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
552 sb.addEventListener('focus', this.handleFocus.bind(this));
554 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
556 window.addEventListener('mouseover', this.setFocus);
557 window.addEventListener('click', this.closeAllDropdowns);
561 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
562 create.addEventListener('focus', this.handleCreateFocus.bind(this));
563 create.addEventListener('blur', this.handleCreateBlur.bind(this));
565 var li = findParent(create, 'li');
567 li.setAttribute('unselectable', '');
568 li.addEventListener('click', this.handleCreateClick.bind(this));
573 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
574 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
576 L.dom.bindClassInstance(sb, this);
581 openDropdown: function(sb) {
582 var st = window.getComputedStyle(sb, null),
583 ul = sb.querySelector('ul'),
584 li = ul.querySelectorAll('li'),
585 fl = findParent(sb, '.cbi-value-field'),
586 sel = ul.querySelector('[selected]'),
587 rect = sb.getBoundingClientRect(),
588 items = Math.min(this.options.dropdown_items, li.length);
590 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
591 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
594 sb.setAttribute('open', '');
596 var pv = ul.cloneNode(true);
597 pv.classList.add('preview');
600 fl.classList.add('cbi-dropdown-open');
602 if ('ontouchstart' in window) {
603 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
604 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
605 scrollFrom = window.pageYOffset,
606 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
609 ul.style.top = sb.offsetHeight + 'px';
610 ul.style.left = -rect.left + 'px';
611 ul.style.right = (rect.right - vpWidth) + 'px';
612 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
613 ul.style.WebkitOverflowScrolling = 'touch';
615 var scrollStep = function(timestamp) {
618 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
621 var duration = Math.max(timestamp - start, 1);
622 if (duration < 100) {
623 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
624 window.requestAnimationFrame(scrollStep);
627 document.body.scrollTop = scrollTo;
631 window.requestAnimationFrame(scrollStep);
634 ul.style.maxHeight = '1px';
635 ul.style.top = ul.style.bottom = '';
637 window.requestAnimationFrame(function() {
638 var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
640 spaceAbove = rect.top,
641 spaceBelow = window.innerHeight - rect.height - rect.top;
643 for (var i = 0; i < (items == -1 ? li.length : items); i++)
644 fullHeight += li[i].getBoundingClientRect().height;
646 if (fullHeight <= spaceBelow) {
647 ul.style.top = rect.height + 'px';
648 ul.style.maxHeight = spaceBelow + 'px';
650 else if (fullHeight <= spaceAbove) {
651 ul.style.bottom = rect.height + 'px';
652 ul.style.maxHeight = spaceAbove + 'px';
654 else if (spaceBelow >= spaceAbove) {
655 ul.style.top = rect.height + 'px';
656 ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
659 ul.style.bottom = rect.height + 'px';
660 ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
663 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
667 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
668 for (var i = 0; i < cboxes.length; i++) {
669 cboxes[i].checked = true;
670 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
673 ul.classList.add('dropdown');
675 sb.insertBefore(pv, ul.nextElementSibling);
677 li.forEach(function(l) {
678 l.setAttribute('tabindex', 0);
681 sb.lastElementChild.setAttribute('tabindex', 0);
683 this.setFocus(sb, sel || li[0], true);
686 closeDropdown: function(sb, no_focus) {
687 if (!sb.hasAttribute('open'))
690 var pv = sb.querySelector('ul.preview'),
691 ul = sb.querySelector('ul.dropdown'),
692 li = ul.querySelectorAll('li'),
693 fl = findParent(sb, '.cbi-value-field');
695 li.forEach(function(l) { l.removeAttribute('tabindex'); });
696 sb.lastElementChild.removeAttribute('tabindex');
699 sb.removeAttribute('open');
700 sb.style.width = sb.style.height = '';
702 ul.classList.remove('dropdown');
703 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
706 fl.classList.remove('cbi-dropdown-open');
709 this.setFocus(sb, sb);
711 this.saveValues(sb, ul);
714 toggleItem: function(sb, li, force_state) {
715 if (li.hasAttribute('unselectable'))
718 if (this.options.multiple) {
719 var cbox = li.querySelector('input[type="checkbox"]'),
720 items = li.parentNode.querySelectorAll('li'),
721 label = sb.querySelector('ul.preview'),
722 sel = li.parentNode.querySelectorAll('[selected]').length,
723 more = sb.querySelector('.more'),
724 ndisplay = this.options.display_items,
727 if (li.hasAttribute('selected')) {
728 if (force_state !== true) {
729 if (sel > 1 || this.options.optional) {
730 li.removeAttribute('selected');
731 cbox.checked = cbox.disabled = false;
735 cbox.disabled = true;
740 if (force_state !== false) {
741 li.setAttribute('selected', '');
743 cbox.disabled = false;
748 while (label && label.firstElementChild)
749 label.removeChild(label.firstElementChild);
751 for (var i = 0; i < items.length; i++) {
752 items[i].removeAttribute('display');
753 if (items[i].hasAttribute('selected')) {
754 if (ndisplay-- > 0) {
755 items[i].setAttribute('display', n++);
757 label.appendChild(items[i].cloneNode(true));
759 var c = items[i].querySelector('input[type="checkbox"]');
761 c.disabled = (sel == 1 && !this.options.optional);
766 sb.setAttribute('more', '');
768 sb.removeAttribute('more');
770 if (ndisplay === this.options.display_items)
771 sb.setAttribute('empty', '');
773 sb.removeAttribute('empty');
775 L.dom.content(more, (ndisplay === this.options.display_items)
776 ? (this.options.select_placeholder || this.options.placeholder) : '···');
779 var sel = li.parentNode.querySelector('[selected]');
781 sel.removeAttribute('display');
782 sel.removeAttribute('selected');
785 li.setAttribute('display', 0);
786 li.setAttribute('selected', '');
788 this.closeDropdown(sb, true);
791 this.saveValues(sb, li.parentNode);
794 transformItem: function(sb, li) {
795 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
798 while (li.firstChild)
799 label.appendChild(li.firstChild);
801 li.appendChild(cbox);
802 li.appendChild(label);
805 saveValues: function(sb, ul) {
806 var sel = ul.querySelectorAll('li[selected]'),
807 div = sb.lastElementChild,
808 name = this.options.name,
812 while (div.lastElementChild)
813 div.removeChild(div.lastElementChild);
815 sel.forEach(function (s) {
816 if (s.hasAttribute('placeholder'))
821 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
825 div.appendChild(E('input', {
833 strval += strval.length ? ' ' + v.value : v.value;
841 if (this.options.multiple)
842 detail.values = values;
844 detail.value = values.length ? values[0] : null;
848 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
854 setValues: function(sb, values) {
855 var ul = sb.querySelector('ul');
857 if (this.options.create) {
858 for (var value in values) {
859 this.createItems(sb, value);
861 if (!this.options.multiple)
866 if (this.options.multiple) {
867 var lis = ul.querySelectorAll('li[data-value]');
868 for (var i = 0; i < lis.length; i++) {
869 var value = lis[i].getAttribute('data-value');
870 if (values === null || !(value in values))
871 this.toggleItem(sb, lis[i], false);
873 this.toggleItem(sb, lis[i], true);
877 var ph = ul.querySelector('li[placeholder]');
879 this.toggleItem(sb, ph);
881 var lis = ul.querySelectorAll('li[data-value]');
882 for (var i = 0; i < lis.length; i++) {
883 var value = lis[i].getAttribute('data-value');
884 if (values !== null && (value in values))
885 this.toggleItem(sb, lis[i]);
890 setFocus: function(sb, elem, scroll) {
891 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
894 if (sb.target && findParent(sb.target, 'ul.dropdown'))
897 document.querySelectorAll('.focus').forEach(function(e) {
898 if (!matchesElem(e, 'input')) {
899 e.classList.remove('focus');
906 elem.classList.add('focus');
909 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
913 createItems: function(sb, value) {
915 val = (value || '').trim(),
916 ul = sb.querySelector('ul');
918 if (!sbox.options.multiple)
919 val = val.length ? [ val ] : [];
921 val = val.length ? val.split(/\s+/) : [];
923 val.forEach(function(item) {
926 ul.childNodes.forEach(function(li) {
927 if (li.getAttribute && li.getAttribute('data-value') === item)
933 tpl = sb.querySelector(sbox.options.create_template);
936 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
938 markup = '<li data-value="{{value}}">{{value}}</li>';
940 new_item = E(markup.replace(/{{value}}/g, '%h'.format(item)));
942 if (sbox.options.multiple) {
943 sbox.transformItem(sb, new_item);
946 var old = ul.querySelector('li[created]');
950 new_item.setAttribute('created', '');
953 new_item = ul.insertBefore(new_item, ul.lastElementChild);
956 sbox.toggleItem(sb, new_item, true);
957 sbox.setFocus(sb, new_item, true);
961 closeAllDropdowns: function() {
962 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
963 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
967 handleClick: function(ev) {
968 var sb = ev.currentTarget;
970 if (!sb.hasAttribute('open')) {
971 if (!matchesElem(ev.target, 'input'))
972 this.openDropdown(sb);
975 var li = findParent(ev.target, 'li');
976 if (li && li.parentNode.classList.contains('dropdown'))
977 this.toggleItem(sb, li);
978 else if (li && li.parentNode.classList.contains('preview'))
979 this.closeDropdown(sb);
980 else if (matchesElem(ev.target, 'span.open, span.more'))
981 this.closeDropdown(sb);
985 ev.stopPropagation();
988 handleKeydown: function(ev) {
989 var sb = ev.currentTarget;
991 if (matchesElem(ev.target, 'input'))
994 if (!sb.hasAttribute('open')) {
995 switch (ev.keyCode) {
1000 this.openDropdown(sb);
1001 ev.preventDefault();
1005 var active = findParent(document.activeElement, 'li');
1007 switch (ev.keyCode) {
1009 this.closeDropdown(sb);
1014 if (!active.hasAttribute('selected'))
1015 this.toggleItem(sb, active);
1016 this.closeDropdown(sb);
1017 ev.preventDefault();
1023 this.toggleItem(sb, active);
1024 ev.preventDefault();
1029 if (active && active.previousElementSibling) {
1030 this.setFocus(sb, active.previousElementSibling);
1031 ev.preventDefault();
1036 if (active && active.nextElementSibling) {
1037 this.setFocus(sb, active.nextElementSibling);
1038 ev.preventDefault();
1045 handleDropdownClose: function(ev) {
1046 var sb = ev.currentTarget;
1048 this.closeDropdown(sb, true);
1051 handleDropdownSelect: function(ev) {
1052 var sb = ev.currentTarget,
1053 li = findParent(ev.target, 'li');
1058 this.toggleItem(sb, li);
1059 this.closeDropdown(sb, true);
1062 handleMouseover: function(ev) {
1063 var sb = ev.currentTarget;
1065 if (!sb.hasAttribute('open'))
1068 var li = findParent(ev.target, 'li');
1070 if (li && li.parentNode.classList.contains('dropdown'))
1071 this.setFocus(sb, li);
1074 handleFocus: function(ev) {
1075 var sb = ev.currentTarget;
1077 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
1078 if (s !== sb || sb.hasAttribute('open'))
1079 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1083 handleCanaryFocus: function(ev) {
1084 this.closeDropdown(ev.currentTarget.parentNode);
1087 handleCreateKeydown: function(ev) {
1088 var input = ev.currentTarget,
1089 sb = findParent(input, '.cbi-dropdown');
1091 switch (ev.keyCode) {
1093 ev.preventDefault();
1095 if (input.classList.contains('cbi-input-invalid'))
1098 this.createItems(sb, input.value);
1105 handleCreateFocus: function(ev) {
1106 var input = ev.currentTarget,
1107 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1108 sb = findParent(input, '.cbi-dropdown');
1111 cbox.checked = true;
1113 sb.setAttribute('locked-in', '');
1116 handleCreateBlur: function(ev) {
1117 var input = ev.currentTarget,
1118 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1119 sb = findParent(input, '.cbi-dropdown');
1122 cbox.checked = false;
1124 sb.removeAttribute('locked-in');
1127 handleCreateClick: function(ev) {
1128 ev.currentTarget.querySelector(this.options.create_query).focus();
1131 setValue: function(values) {
1132 if (this.options.multiple) {
1133 if (!Array.isArray(values))
1134 values = (values != null && values != '') ? [ values ] : [];
1138 for (var i = 0; i < values.length; i++)
1139 v[values[i]] = true;
1141 this.setValues(this.node, v);
1146 if (values != null) {
1147 if (Array.isArray(values))
1148 v[values[0]] = true;
1153 this.setValues(this.node, v);
1157 getValue: function() {
1158 var div = this.node.lastElementChild,
1159 h = div.querySelectorAll('input[type="hidden"]'),
1162 for (var i = 0; i < h.length; i++)
1165 return this.options.multiple ? v : v[0];
1169 var UICombobox = UIDropdown.extend({
1170 __init__: function(value, choices, options) {
1171 this.super('__init__', [ value, choices, Object.assign({
1172 select_placeholder: _('-- Please choose --'),
1173 custom_placeholder: _('-- custom --'),
1184 var UIDynamicList = UIElement.extend({
1185 __init__: function(values, choices, options) {
1186 if (!Array.isArray(values))
1187 values = (values != null && values != '') ? [ values ] : [];
1189 if (typeof(choices) != 'object')
1192 this.values = values;
1193 this.choices = choices;
1194 this.options = Object.assign({}, options, {
1200 render: function() {
1202 'id': this.options.id,
1203 'class': 'cbi-dynlist'
1204 }, E('div', { 'class': 'add-item' }));
1207 var cbox = new UICombobox(null, this.choices, this.options);
1208 dl.lastElementChild.appendChild(cbox.render());
1211 var inputEl = E('input', {
1212 'id': this.options.id ? 'widget.' + this.options.id : null,
1214 'class': 'cbi-input-text',
1215 'placeholder': this.options.placeholder
1218 dl.lastElementChild.appendChild(inputEl);
1219 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1221 if (this.options.datatype)
1222 L.ui.addValidator(inputEl, this.options.datatype,
1223 true, null, 'blur', 'keyup');
1226 for (var i = 0; i < this.values.length; i++)
1227 this.addItem(dl, this.values[i],
1228 this.choices ? this.choices[this.values[i]] : null);
1230 return this.bind(dl);
1233 bind: function(dl) {
1234 dl.addEventListener('click', L.bind(this.handleClick, this));
1235 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
1236 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
1240 this.setUpdateEvents(dl, 'cbi-dynlist-change');
1241 this.setChangeEvents(dl, 'cbi-dynlist-change');
1243 L.dom.bindClassInstance(dl, this);
1248 addItem: function(dl, value, text, flash) {
1250 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
1251 E('span', {}, text || value),
1254 'name': this.options.name,
1255 'value': value })]);
1257 dl.querySelectorAll('.item').forEach(function(item) {
1261 var hidden = item.querySelector('input[type="hidden"]');
1263 if (hidden && hidden.parentNode !== item)
1266 if (hidden && hidden.value === value)
1271 var ai = dl.querySelector('.add-item');
1272 ai.parentNode.insertBefore(new_item, ai);
1275 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1286 removeItem: function(dl, item) {
1287 var value = item.querySelector('input[type="hidden"]').value;
1288 var sb = dl.querySelector('.cbi-dropdown');
1290 sb.querySelectorAll('ul > li').forEach(function(li) {
1291 if (li.getAttribute('data-value') === value) {
1292 if (li.hasAttribute('dynlistcustom'))
1293 li.parentNode.removeChild(li);
1295 li.removeAttribute('unselectable');
1299 item.parentNode.removeChild(item);
1301 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1312 handleClick: function(ev) {
1313 var dl = ev.currentTarget,
1314 item = findParent(ev.target, '.item');
1317 this.removeItem(dl, item);
1319 else if (matchesElem(ev.target, '.cbi-button-add')) {
1320 var input = ev.target.previousElementSibling;
1321 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
1322 this.addItem(dl, input.value, null, true);
1328 handleDropdownChange: function(ev) {
1329 var dl = ev.currentTarget,
1330 sbIn = ev.detail.instance,
1331 sbEl = ev.detail.element,
1332 sbVal = ev.detail.value;
1337 sbIn.setValues(sbEl, null);
1338 sbVal.element.setAttribute('unselectable', '');
1340 if (sbVal.element.hasAttribute('created')) {
1341 sbVal.element.removeAttribute('created');
1342 sbVal.element.setAttribute('dynlistcustom', '');
1345 this.addItem(dl, sbVal.value, sbVal.text, true);
1348 handleKeydown: function(ev) {
1349 var dl = ev.currentTarget,
1350 item = findParent(ev.target, '.item');
1353 switch (ev.keyCode) {
1354 case 8: /* backspace */
1355 if (item.previousElementSibling)
1356 item.previousElementSibling.focus();
1358 this.removeItem(dl, item);
1361 case 46: /* delete */
1362 if (item.nextElementSibling) {
1363 if (item.nextElementSibling.classList.contains('item'))
1364 item.nextElementSibling.focus();
1366 item.nextElementSibling.firstElementChild.focus();
1369 this.removeItem(dl, item);
1373 else if (matchesElem(ev.target, '.cbi-input-text')) {
1374 switch (ev.keyCode) {
1375 case 13: /* enter */
1376 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
1377 this.addItem(dl, ev.target.value, null, true);
1378 ev.target.value = '';
1383 ev.preventDefault();
1389 getValue: function() {
1390 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1391 input = this.node.querySelector('.add-item > input[type="text"]'),
1394 for (var i = 0; i < items.length; i++)
1395 v.push(items[i].value);
1397 if (input && input.value != null && input.value.match(/\S/) &&
1398 input.classList.contains('cbi-input-invalid') == false &&
1399 v.filter(function(s) { return s == input.value }).length == 0)
1400 v.push(input.value);
1405 setValue: function(values) {
1406 if (!Array.isArray(values))
1407 values = (values != null && values != '') ? [ values ] : [];
1409 var items = this.node.querySelectorAll('.item');
1411 for (var i = 0; i < items.length; i++)
1412 if (items[i].parentNode === this.node)
1413 this.removeItem(this.node, items[i]);
1415 for (var i = 0; i < values.length; i++)
1416 this.addItem(this.node, values[i],
1417 this.choices ? this.choices[values[i]] : null);
1421 var UIHiddenfield = UIElement.extend({
1422 __init__: function(value, options) {
1424 this.options = Object.assign({
1429 render: function() {
1430 var hiddenEl = E('input', {
1431 'id': this.options.id,
1436 return this.bind(hiddenEl);
1439 bind: function(hiddenEl) {
1440 this.node = hiddenEl;
1442 L.dom.bindClassInstance(hiddenEl, this);
1447 getValue: function() {
1448 return this.node.value;
1451 setValue: function(value) {
1452 this.node.value = value;
1457 return L.Class.extend({
1458 __init__: function() {
1459 modalDiv = document.body.appendChild(
1460 L.dom.create('div', { id: 'modal_overlay' },
1461 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1463 tooltipDiv = document.body.appendChild(
1464 L.dom.create('div', { class: 'cbi-tooltip' }));
1466 /* setup old aliases */
1467 L.showModal = this.showModal;
1468 L.hideModal = this.hideModal;
1469 L.showTooltip = this.showTooltip;
1470 L.hideTooltip = this.hideTooltip;
1471 L.itemlist = this.itemlist;
1473 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1474 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1475 document.addEventListener('focus', this.showTooltip.bind(this), true);
1476 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1478 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1479 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
1480 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
1484 showModal: function(title, children /* , ... */) {
1485 var dlg = modalDiv.firstElementChild;
1487 dlg.setAttribute('class', 'modal');
1489 for (var i = 2; i < arguments.length; i++)
1490 dlg.classList.add(arguments[i]);
1492 L.dom.content(dlg, L.dom.create('h4', {}, title));
1493 L.dom.append(dlg, children);
1495 document.body.classList.add('modal-overlay-active');
1500 hideModal: function() {
1501 document.body.classList.remove('modal-overlay-active');
1505 showTooltip: function(ev) {
1506 var target = findParent(ev.target, '[data-tooltip]');
1511 if (tooltipTimeout !== null) {
1512 window.clearTimeout(tooltipTimeout);
1513 tooltipTimeout = null;
1516 var rect = target.getBoundingClientRect(),
1517 x = rect.left + window.pageXOffset,
1518 y = rect.top + rect.height + window.pageYOffset;
1520 tooltipDiv.className = 'cbi-tooltip';
1521 tooltipDiv.innerHTML = '▲ ';
1522 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1524 if (target.hasAttribute('data-tooltip-style'))
1525 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1527 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1528 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1529 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1532 tooltipDiv.style.top = y + 'px';
1533 tooltipDiv.style.left = x + 'px';
1534 tooltipDiv.style.opacity = 1;
1536 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1538 detail: { target: target }
1542 hideTooltip: function(ev) {
1543 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1544 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1547 if (tooltipTimeout !== null) {
1548 window.clearTimeout(tooltipTimeout);
1549 tooltipTimeout = null;
1552 tooltipDiv.style.opacity = 0;
1553 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1555 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1559 itemlist: function(node, items, separators) {
1562 if (!Array.isArray(separators))
1563 separators = [ separators || E('br') ];
1565 for (var i = 0; i < items.length; i += 2) {
1566 if (items[i+1] !== null && items[i+1] !== undefined) {
1567 var sep = separators[(i/2) % separators.length],
1570 children.push(E('span', { class: 'nowrap' }, [
1571 items[i] ? E('strong', items[i] + ': ') : '',
1575 if ((i+2) < items.length)
1576 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
1580 L.dom.content(node, children);
1586 tabs: L.Class.singleton({
1588 var groups = [], prevGroup = null, currGroup = null;
1590 document.querySelectorAll('[data-tab]').forEach(function(tab) {
1591 var parent = tab.parentNode;
1593 if (!parent.hasAttribute('data-tab-group'))
1594 parent.setAttribute('data-tab-group', groups.length);
1596 currGroup = +parent.getAttribute('data-tab-group');
1598 if (currGroup !== prevGroup) {
1599 prevGroup = currGroup;
1601 if (!groups[currGroup])
1602 groups[currGroup] = [];
1605 groups[currGroup].push(tab);
1608 for (var i = 0; i < groups.length; i++)
1609 this.initTabGroup(groups[i]);
1611 document.addEventListener('dependency-update', this.updateTabs.bind(this));
1616 initTabGroup: function(panes) {
1617 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
1620 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
1621 group = panes[0].parentNode,
1622 groupId = +group.getAttribute('data-tab-group'),
1625 for (var i = 0, pane; pane = panes[i]; i++) {
1626 var name = pane.getAttribute('data-tab'),
1627 title = pane.getAttribute('data-tab-title'),
1628 active = pane.getAttribute('data-tab-active') === 'true';
1630 menu.appendChild(E('li', {
1631 'style': this.isEmptyPane(pane) ? 'display:none' : null,
1632 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
1636 'click': this.switchTab.bind(this)
1643 group.parentNode.insertBefore(menu, group);
1645 if (selected === null) {
1646 selected = this.getActiveTabId(panes[0]);
1648 if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
1649 for (var i = 0; i < panes.length; i++) {
1650 if (!this.isEmptyPane(panes[i])) {
1657 menu.childNodes[selected].classList.add('cbi-tab');
1658 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
1659 panes[selected].setAttribute('data-tab-active', 'true');
1661 this.setActiveTabId(panes[selected], selected);
1664 this.updateTabs(group);
1667 isEmptyPane: function(pane) {
1668 return L.dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
1671 getPathForPane: function(pane) {
1672 var path = [], node = null;
1674 for (node = pane ? pane.parentNode : null;
1675 node != null && node.hasAttribute != null;
1676 node = node.parentNode)
1678 if (node.hasAttribute('data-tab'))
1679 path.unshift(node.getAttribute('data-tab'));
1680 else if (node.hasAttribute('data-section-id'))
1681 path.unshift(node.getAttribute('data-section-id'));
1684 return path.join('/');
1687 getActiveTabState: function() {
1688 var page = document.body.getAttribute('data-page');
1691 var val = JSON.parse(window.sessionStorage.getItem('tab'));
1692 if (val.page === page && L.isObject(val.paths))
1697 window.sessionStorage.removeItem('tab');
1698 return { page: page, paths: {} };
1701 getActiveTabId: function(pane) {
1702 var path = this.getPathForPane(pane);
1703 return +this.getActiveTabState().paths[path] || 0;
1706 setActiveTabId: function(pane, tabIndex) {
1707 var path = this.getPathForPane(pane);
1710 var state = this.getActiveTabState();
1711 state.paths[path] = tabIndex;
1713 window.sessionStorage.setItem('tab', JSON.stringify(state));
1715 catch (e) { return false; }
1720 updateTabs: function(ev, root) {
1721 (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
1722 var menu = pane.parentNode.previousElementSibling,
1723 tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
1724 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
1729 if (this.isEmptyPane(pane)) {
1730 tab.style.display = 'none';
1731 tab.classList.remove('flash');
1733 else if (tab.style.display === 'none') {
1734 tab.style.display = '';
1735 requestAnimationFrame(function() { tab.classList.add('flash') });
1739 tab.setAttribute('data-errors', n_errors);
1740 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
1741 tab.setAttribute('data-tooltip-style', 'error');
1744 tab.removeAttribute('data-errors');
1745 tab.removeAttribute('data-tooltip');
1750 switchTab: function(ev) {
1751 var tab = ev.target.parentNode,
1752 name = tab.getAttribute('data-tab'),
1753 menu = tab.parentNode,
1754 group = menu.nextElementSibling,
1755 groupId = +group.getAttribute('data-tab-group'),
1758 ev.preventDefault();
1760 if (!tab.classList.contains('cbi-tab-disabled'))
1763 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
1764 tab.classList.remove('cbi-tab');
1765 tab.classList.remove('cbi-tab-disabled');
1767 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
1770 group.childNodes.forEach(function(pane) {
1771 if (L.dom.matches(pane, '[data-tab]')) {
1772 if (pane.getAttribute('data-tab') === name) {
1773 pane.setAttribute('data-tab-active', 'true');
1774 L.ui.tabs.setActiveTabId(pane, index);
1777 pane.setAttribute('data-tab-active', 'false');
1787 changes: L.Class.singleton({
1789 if (!L.env.sessionid)
1792 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
1795 setIndicator: function(n) {
1796 var i = document.querySelector('.uci_change_indicator');
1798 var poll = document.getElementById('xhr_poll_status');
1799 i = poll.parentNode.insertBefore(E('a', {
1801 'class': 'uci_change_indicator label notice',
1802 'click': L.bind(this.displayChanges, this)
1807 L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
1808 i.classList.add('flash');
1809 i.style.display = '';
1812 i.classList.remove('flash');
1813 i.style.display = 'none';
1817 renderChangeIndicator: function(changes) {
1820 for (var config in changes)
1821 if (changes.hasOwnProperty(config))
1822 n_changes += changes[config].length;
1824 this.changes = changes;
1825 this.setIndicator(n_changes);
1829 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1830 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1831 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1832 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1833 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1834 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1835 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1836 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1837 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1838 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1841 displayChanges: function() {
1842 var list = E('div', { 'class': 'uci-change-list' }),
1843 dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
1844 E('div', { 'class': 'cbi-section' }, [
1845 E('strong', _('Legend:')),
1846 E('div', { 'class': 'uci-change-legend' }, [
1847 E('div', { 'class': 'uci-change-legend-label' }, [
1848 E('ins', ' '), ' ', _('Section added') ]),
1849 E('div', { 'class': 'uci-change-legend-label' }, [
1850 E('del', ' '), ' ', _('Section removed') ]),
1851 E('div', { 'class': 'uci-change-legend-label' }, [
1852 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1853 E('div', { 'class': 'uci-change-legend-label' }, [
1854 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1856 E('div', { 'class': 'right' }, [
1860 'click': L.ui.hideModal,
1861 'value': _('Dismiss')
1865 'class': 'cbi-button cbi-button-positive important',
1866 'click': L.bind(this.apply, this, true),
1867 'value': _('Save & Apply')
1871 'class': 'cbi-button cbi-button-reset',
1872 'click': L.bind(this.revert, this),
1873 'value': _('Revert')
1877 for (var config in this.changes) {
1878 if (!this.changes.hasOwnProperty(config))
1881 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
1883 for (var i = 0, added = null; i < this.changes[config].length; i++) {
1884 var chg = this.changes[config][i],
1885 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
1887 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
1893 if (added != null && chg[1] == added[0])
1894 return '@' + added[1] + '[-1]';
1899 return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
1906 if (chg[0] == 'add')
1907 added = [ chg[1], chg[2] ];
1911 list.appendChild(E('br'));
1912 dlg.classList.add('uci-dialog');
1915 displayStatus: function(type, content) {
1917 var message = L.ui.showModal('', '');
1919 message.classList.add('alert-message');
1920 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1923 L.dom.content(message, content);
1925 if (!this.was_polling) {
1926 this.was_polling = L.Request.poll.active();
1927 L.Request.poll.stop();
1933 if (this.was_polling)
1934 L.Request.poll.start();
1938 rollback: function(checked) {
1940 this.displayStatus('warning spinning',
1941 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
1942 .format(L.env.apply_rollback)));
1944 var call = function(r, data, duration) {
1945 if (r.status === 204) {
1946 L.ui.changes.displayStatus('warning', [
1947 E('h4', _('Configuration has been rolled back!')),
1948 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)),
1949 E('div', { 'class': 'right' }, [
1953 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1954 'value': _('Dismiss')
1958 'class': 'btn cbi-button-action important',
1959 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1960 'value': _('Revert changes')
1964 'class': 'btn cbi-button-negative important',
1965 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1966 'value': _('Apply unchecked')
1974 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1975 window.setTimeout(function() {
1976 L.Request.request(L.url('admin/uci/confirm'), {
1978 timeout: L.env.apply_timeout * 1000,
1979 query: { sid: L.env.sessionid, token: L.env.token }
1984 call({ status: 0 });
1987 this.displayStatus('warning', [
1988 E('h4', _('Device unreachable!')),
1989 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.'))
1994 confirm: function(checked, deadline, override_token) {
1996 var ts = Date.now();
1998 this.displayStatus('notice');
2001 this.confirm_auth = { token: override_token };
2003 var call = function(r, data, duration) {
2004 if (Date.now() >= deadline) {
2005 window.clearTimeout(tt);
2006 L.ui.changes.rollback(checked);
2009 else if (r && (r.status === 200 || r.status === 204)) {
2010 document.dispatchEvent(new CustomEvent('uci-applied'));
2012 L.ui.changes.setIndicator(0);
2013 L.ui.changes.displayStatus('notice',
2014 E('p', _('Configuration has been applied.')));
2016 window.clearTimeout(tt);
2017 window.setTimeout(function() {
2018 //L.ui.changes.displayStatus(false);
2019 window.location = window.location.href.split('#')[0];
2020 }, L.env.apply_display * 1000);
2025 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2026 window.setTimeout(function() {
2027 L.Request.request(L.url('admin/uci/confirm'), {
2029 timeout: L.env.apply_timeout * 1000,
2030 query: L.ui.changes.confirm_auth
2031 }).then(call, call);
2035 var tick = function() {
2036 var now = Date.now();
2038 L.ui.changes.displayStatus('notice spinning',
2039 E('p', _('Waiting for configuration to get applied… %ds')
2040 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2042 if (now >= deadline)
2045 tt = window.setTimeout(tick, 1000 - (now - ts));
2051 /* wait a few seconds for the settings to become effective */
2052 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2055 apply: function(checked) {
2056 this.displayStatus('notice spinning',
2057 E('p', _('Starting configuration apply…')));
2059 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2061 query: { sid: L.env.sessionid, token: L.env.token }
2062 }).then(function(r) {
2063 if (r.status === (checked ? 200 : 204)) {
2064 var tok = null; try { tok = r.json(); } catch(e) {}
2065 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2066 L.ui.changes.confirm_auth = tok;
2068 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2070 else if (checked && r.status === 204) {
2071 L.ui.changes.displayStatus('notice',
2072 E('p', _('There are no changes to apply')));
2074 window.setTimeout(function() {
2075 L.ui.changes.displayStatus(false);
2076 }, L.env.apply_display * 1000);
2079 L.ui.changes.displayStatus('warning',
2080 E('p', _('Apply request failed with status <code>%h</code>')
2081 .format(r.responseText || r.statusText || r.status)));
2083 window.setTimeout(function() {
2084 L.ui.changes.displayStatus(false);
2085 }, L.env.apply_display * 1000);
2090 revert: function() {
2091 this.displayStatus('notice spinning',
2092 E('p', _('Reverting configuration…')));
2094 L.Request.request(L.url('admin/uci/revert'), {
2096 query: { sid: L.env.sessionid, token: L.env.token }
2097 }).then(function(r) {
2098 if (r.status === 200) {
2099 document.dispatchEvent(new CustomEvent('uci-reverted'));
2101 L.ui.changes.setIndicator(0);
2102 L.ui.changes.displayStatus('notice',
2103 E('p', _('Changes have been reverted.')));
2105 window.setTimeout(function() {
2106 //L.ui.changes.displayStatus(false);
2107 window.location = window.location.href.split('#')[0];
2108 }, L.env.apply_display * 1000);
2111 L.ui.changes.displayStatus('warning',
2112 E('p', _('Revert request failed with status <code>%h</code>')
2113 .format(r.statusText || r.status)));
2115 window.setTimeout(function() {
2116 L.ui.changes.displayStatus(false);
2117 }, L.env.apply_display * 1000);
2123 addValidator: function(field, type, optional, vfunc /*, ... */) {
2127 var events = this.varargs(arguments, 3);
2128 if (events.length == 0)
2129 events.push('blur', 'keyup');
2132 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2133 validatorFn = cbiValidator.validate.bind(cbiValidator);
2135 for (var i = 0; i < events.length; i++)
2136 field.addEventListener(events[i], validatorFn);
2145 createHandlerFn: function(ctx, fn /*, ... */) {
2146 if (typeof(fn) == 'string')
2149 if (typeof(fn) != 'function')
2152 return Function.prototype.bind.apply(function() {
2153 var t = arguments[arguments.length - 1].target;
2155 t.classList.add('spinning');
2161 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2162 t.classList.remove('spinning');
2165 }, this.varargs(arguments, 2, ctx));
2169 Textfield: UITextfield,
2170 Textarea: UITextarea,
2171 Checkbox: UICheckbox,
2173 Dropdown: UIDropdown,
2174 DynamicList: UIDynamicList,
2175 Combobox: UICombobox,
2176 Hiddenfield: UIHiddenfield