11 var UIElement = L.Class.extend({
12 getValue: function() {
13 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
14 return this.node.value;
19 setValue: function(value) {
20 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
21 this.node.value = value;
25 return (this.validState !== false);
28 triggerValidation: function() {
29 if (typeof(this.vfunc) != 'function')
32 var wasValid = this.isValid();
36 return (wasValid != this.isValid());
39 registerEvents: function(targetNode, synevent, events) {
40 var dispatchFn = L.bind(function(ev) {
41 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
44 for (var i = 0; i < events.length; i++)
45 targetNode.addEventListener(events[i], dispatchFn);
48 setUpdateEvents: function(targetNode /*, ... */) {
49 var datatype = this.options.datatype,
50 optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
51 validate = this.options.validate,
52 events = this.varargs(arguments, 1);
54 this.registerEvents(targetNode, 'widget-update', events);
56 if (!datatype && !validate)
59 this.vfunc = L.ui.addValidator.apply(L.ui, [
60 targetNode, datatype || 'string',
64 this.node.addEventListener('validation-success', L.bind(function(ev) {
65 this.validState = true;
68 this.node.addEventListener('validation-failure', L.bind(function(ev) {
69 this.validState = false;
73 setChangeEvents: function(targetNode /*, ... */) {
74 var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
76 for (var i = 1; i < arguments.length; i++)
77 targetNode.addEventListener(arguments[i], tag_changed);
79 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
83 var UITextfield = UIElement.extend({
84 __init__: function(value, options) {
86 this.options = Object.assign({
93 var frameEl = E('div', { 'id': this.options.id });
95 if (this.options.password) {
96 frameEl.classList.add('nowrap');
97 frameEl.appendChild(E('input', {
99 'style': 'position:absolute; left:-100000px',
102 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
106 frameEl.appendChild(E('input', {
107 'id': this.options.id ? 'widget.' + this.options.id : null,
108 'name': this.options.name,
109 'type': this.options.password ? 'password' : 'text',
110 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
111 'readonly': this.options.readonly ? '' : null,
112 'maxlength': this.options.maxlength,
113 'placeholder': this.options.placeholder,
117 if (this.options.password)
118 frameEl.appendChild(E('button', {
119 'class': 'cbi-button cbi-button-neutral',
120 'title': _('Reveal/hide password'),
121 'aria-label': _('Reveal/hide password'),
122 'click': function(ev) {
123 var e = this.previousElementSibling;
124 e.type = (e.type === 'password') ? 'text' : 'password';
129 return this.bind(frameEl);
132 bind: function(frameEl) {
133 var inputEl = frameEl.childNodes[+!!this.options.password];
137 this.setUpdateEvents(inputEl, 'keyup', 'blur');
138 this.setChangeEvents(inputEl, 'change');
140 L.dom.bindClassInstance(frameEl, this);
145 getValue: function() {
146 var inputEl = this.node.childNodes[+!!this.options.password];
147 return inputEl.value;
150 setValue: function(value) {
151 var inputEl = this.node.childNodes[+!!this.options.password];
152 inputEl.value = value;
156 var UITextarea = UIElement.extend({
157 __init__: function(value, options) {
159 this.options = Object.assign({
168 var frameEl = E('div', { 'id': this.options.id }),
169 value = (this.value != null) ? String(this.value) : '';
171 frameEl.appendChild(E('textarea', {
172 'id': this.options.id ? 'widget.' + this.options.id : null,
173 'name': this.options.name,
174 'class': 'cbi-input-textarea',
175 'readonly': this.options.readonly ? '' : null,
176 'placeholder': this.options.placeholder,
177 'style': !this.options.cols ? 'width:100%' : null,
178 'cols': this.options.cols,
179 'rows': this.options.rows,
180 'wrap': this.options.wrap ? '' : null
183 if (this.options.monospace)
184 frameEl.firstElementChild.style.fontFamily = 'monospace';
186 return this.bind(frameEl);
189 bind: function(frameEl) {
190 var inputEl = frameEl.firstElementChild;
194 this.setUpdateEvents(inputEl, 'keyup', 'blur');
195 this.setChangeEvents(inputEl, 'change');
197 L.dom.bindClassInstance(frameEl, this);
202 getValue: function() {
203 return this.node.firstElementChild.value;
206 setValue: function(value) {
207 this.node.firstElementChild.value = value;
211 var UICheckbox = UIElement.extend({
212 __init__: function(value, options) {
214 this.options = Object.assign({
221 var frameEl = E('div', {
222 'id': this.options.id,
223 'class': 'cbi-checkbox'
226 if (this.options.hiddenname)
227 frameEl.appendChild(E('input', {
229 'name': this.options.hiddenname,
233 frameEl.appendChild(E('input', {
234 'id': this.options.id ? 'widget.' + this.options.id : null,
235 'name': this.options.name,
237 'value': this.options.value_enabled,
238 'checked': (this.value == this.options.value_enabled) ? '' : null
241 return this.bind(frameEl);
244 bind: function(frameEl) {
247 this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
248 this.setChangeEvents(frameEl.lastElementChild, 'change');
250 L.dom.bindClassInstance(frameEl, this);
255 isChecked: function() {
256 return this.node.lastElementChild.checked;
259 getValue: function() {
260 return this.isChecked()
261 ? this.options.value_enabled
262 : this.options.value_disabled;
265 setValue: function(value) {
266 this.node.lastElementChild.checked = (value == this.options.value_enabled);
270 var UISelect = UIElement.extend({
271 __init__: function(value, choices, options) {
272 if (!L.isObject(choices))
275 if (!Array.isArray(value))
276 value = (value != null && value != '') ? [ value ] : [];
278 if (!options.multiple && value.length > 1)
282 this.choices = choices;
283 this.options = Object.assign({
286 orientation: 'horizontal'
289 if (this.choices.hasOwnProperty(''))
290 this.options.optional = true;
294 var frameEl = E('div', { 'id': this.options.id }),
295 keys = Object.keys(this.choices);
297 if (this.options.sort === true)
299 else if (Array.isArray(this.options.sort))
300 keys = this.options.sort;
302 if (this.options.widget == 'select') {
303 frameEl.appendChild(E('select', {
304 'id': this.options.id ? 'widget.' + this.options.id : null,
305 'name': this.options.name,
306 'size': this.options.size,
307 'class': 'cbi-input-select',
308 'multiple': this.options.multiple ? '' : null
311 if (this.options.optional)
312 frameEl.lastChild.appendChild(E('option', {
314 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
315 }, [ this.choices[''] || this.options.placeholder || _('-- Please choose --') ]));
317 for (var i = 0; i < keys.length; i++) {
318 if (keys[i] == null || keys[i] == '')
321 frameEl.lastChild.appendChild(E('option', {
323 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
324 }, [ this.choices[keys[i]] || keys[i] ]));
328 var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
330 for (var i = 0; i < keys.length; i++) {
331 frameEl.appendChild(E('label', {}, [
333 'id': this.options.id ? 'widget.' + this.options.id : null,
334 'name': this.options.id || this.options.name,
335 'type': this.options.multiple ? 'checkbox' : 'radio',
336 'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
338 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
340 this.choices[keys[i]] || keys[i]
343 if (i + 1 == this.options.size)
344 frameEl.appendChild(brEl);
348 return this.bind(frameEl);
351 bind: function(frameEl) {
354 if (this.options.widget == 'select') {
355 this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
356 this.setChangeEvents(frameEl.firstChild, 'change');
359 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
360 for (var i = 0; i < radioEls.length; i++) {
361 this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
362 this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
366 L.dom.bindClassInstance(frameEl, this);
371 getValue: function() {
372 if (this.options.widget == 'select')
373 return this.node.firstChild.value;
375 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
376 for (var i = 0; i < radioEls.length; i++)
377 if (radioEls[i].checked)
378 return radioEls[i].value;
383 setValue: function(value) {
384 if (this.options.widget == 'select') {
388 for (var i = 0; i < this.node.firstChild.options.length; i++)
389 this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
394 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
395 for (var i = 0; i < radioEls.length; i++)
396 radioEls[i].checked = (radioEls[i].value == value);
400 var UIDropdown = UIElement.extend({
401 __init__: function(value, choices, options) {
402 if (typeof(choices) != 'object')
405 if (!Array.isArray(value))
406 this.values = (value != null && value != '') ? [ value ] : [];
410 this.choices = choices;
411 this.options = Object.assign({
413 multiple: Array.isArray(value),
415 select_placeholder: _('-- Please choose --'),
416 custom_placeholder: _('-- custom --'),
420 create_query: '.create-item-input',
421 create_template: 'script[type="item-template"]'
427 'id': this.options.id,
428 'class': 'cbi-dropdown',
429 'multiple': this.options.multiple ? '' : null,
430 'optional': this.options.optional ? '' : null,
433 var keys = Object.keys(this.choices);
435 if (this.options.sort === true)
437 else if (Array.isArray(this.options.sort))
438 keys = this.options.sort;
440 if (this.options.create)
441 for (var i = 0; i < this.values.length; i++)
442 if (!this.choices.hasOwnProperty(this.values[i]))
443 keys.push(this.values[i]);
445 for (var i = 0; i < keys.length; i++)
446 sb.lastElementChild.appendChild(E('li', {
447 'data-value': keys[i],
448 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
449 }, this.choices[keys[i]] || keys[i]));
451 if (this.options.create) {
452 var createEl = E('input', {
454 'class': 'create-item-input',
455 'readonly': this.options.readonly ? '' : null,
456 'maxlength': this.options.maxlength,
457 'placeholder': this.options.custom_placeholder || this.options.placeholder
460 if (this.options.datatype)
461 L.ui.addValidator(createEl, this.options.datatype,
462 true, null, 'blur', 'keyup');
464 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
467 if (this.options.create_markup)
468 sb.appendChild(E('script', { type: 'item-template' },
469 this.options.create_markup));
471 return this.bind(sb);
475 var o = this.options;
477 o.multiple = sb.hasAttribute('multiple');
478 o.optional = sb.hasAttribute('optional');
479 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
480 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
481 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
482 o.create_query = sb.getAttribute('item-create') || o.create_query;
483 o.create_template = sb.getAttribute('item-template') || o.create_template;
485 var ul = sb.querySelector('ul'),
486 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
487 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
488 canary = sb.appendChild(E('div')),
489 create = sb.querySelector(this.options.create_query),
490 ndisplay = this.options.display_items,
493 if (this.options.multiple) {
494 var items = ul.querySelectorAll('li');
496 for (var i = 0; i < items.length; i++) {
497 this.transformItem(sb, items[i]);
499 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
500 items[i].setAttribute('display', n++);
504 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
505 var placeholder = E('li', { placeholder: '' },
506 this.options.select_placeholder || this.options.placeholder);
509 ? ul.insertBefore(placeholder, ul.firstChild)
510 : ul.appendChild(placeholder);
513 var items = ul.querySelectorAll('li'),
514 sel = sb.querySelectorAll('[selected]');
516 sel.forEach(function(s) {
517 s.removeAttribute('selected');
520 var s = sel[0] || items[0];
522 s.setAttribute('selected', '');
523 s.setAttribute('display', n++);
529 this.saveValues(sb, ul);
531 ul.setAttribute('tabindex', -1);
532 sb.setAttribute('tabindex', 0);
535 sb.setAttribute('more', '')
537 sb.removeAttribute('more');
539 if (ndisplay == this.options.display_items)
540 sb.setAttribute('empty', '')
542 sb.removeAttribute('empty');
544 L.dom.content(more, (ndisplay == this.options.display_items)
545 ? (this.options.select_placeholder || this.options.placeholder) : '···');
548 sb.addEventListener('click', this.handleClick.bind(this));
549 sb.addEventListener('keydown', this.handleKeydown.bind(this));
550 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
551 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
553 if ('ontouchstart' in window) {
554 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
555 window.addEventListener('touchstart', this.closeAllDropdowns);
558 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
559 sb.addEventListener('focus', this.handleFocus.bind(this));
561 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
563 window.addEventListener('mouseover', this.setFocus);
564 window.addEventListener('click', this.closeAllDropdowns);
568 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
569 create.addEventListener('focus', this.handleCreateFocus.bind(this));
570 create.addEventListener('blur', this.handleCreateBlur.bind(this));
572 var li = findParent(create, 'li');
574 li.setAttribute('unselectable', '');
575 li.addEventListener('click', this.handleCreateClick.bind(this));
580 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
581 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
583 L.dom.bindClassInstance(sb, this);
588 openDropdown: function(sb) {
589 var st = window.getComputedStyle(sb, null),
590 ul = sb.querySelector('ul'),
591 li = ul.querySelectorAll('li'),
592 fl = findParent(sb, '.cbi-value-field'),
593 sel = ul.querySelector('[selected]'),
594 rect = sb.getBoundingClientRect(),
595 items = Math.min(this.options.dropdown_items, li.length);
597 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
598 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
601 sb.setAttribute('open', '');
603 var pv = ul.cloneNode(true);
604 pv.classList.add('preview');
607 fl.classList.add('cbi-dropdown-open');
609 if ('ontouchstart' in window) {
610 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
611 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
612 scrollFrom = window.pageYOffset,
613 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
616 ul.style.top = sb.offsetHeight + 'px';
617 ul.style.left = -rect.left + 'px';
618 ul.style.right = (rect.right - vpWidth) + 'px';
619 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
620 ul.style.WebkitOverflowScrolling = 'touch';
622 var scrollStep = function(timestamp) {
625 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
628 var duration = Math.max(timestamp - start, 1);
629 if (duration < 100) {
630 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
631 window.requestAnimationFrame(scrollStep);
634 document.body.scrollTop = scrollTo;
638 window.requestAnimationFrame(scrollStep);
641 ul.style.maxHeight = '1px';
642 ul.style.top = ul.style.bottom = '';
644 window.requestAnimationFrame(function() {
645 var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
647 spaceAbove = rect.top,
648 spaceBelow = window.innerHeight - rect.height - rect.top;
650 for (var i = 0; i < (items == -1 ? li.length : items); i++)
651 fullHeight += li[i].getBoundingClientRect().height;
653 if (fullHeight <= spaceBelow) {
654 ul.style.top = rect.height + 'px';
655 ul.style.maxHeight = spaceBelow + 'px';
657 else if (fullHeight <= spaceAbove) {
658 ul.style.bottom = rect.height + 'px';
659 ul.style.maxHeight = spaceAbove + 'px';
661 else if (spaceBelow >= spaceAbove) {
662 ul.style.top = rect.height + 'px';
663 ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
666 ul.style.bottom = rect.height + 'px';
667 ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
670 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
674 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
675 for (var i = 0; i < cboxes.length; i++) {
676 cboxes[i].checked = true;
677 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
680 ul.classList.add('dropdown');
682 sb.insertBefore(pv, ul.nextElementSibling);
684 li.forEach(function(l) {
685 l.setAttribute('tabindex', 0);
688 sb.lastElementChild.setAttribute('tabindex', 0);
690 this.setFocus(sb, sel || li[0], true);
693 closeDropdown: function(sb, no_focus) {
694 if (!sb.hasAttribute('open'))
697 var pv = sb.querySelector('ul.preview'),
698 ul = sb.querySelector('ul.dropdown'),
699 li = ul.querySelectorAll('li'),
700 fl = findParent(sb, '.cbi-value-field');
702 li.forEach(function(l) { l.removeAttribute('tabindex'); });
703 sb.lastElementChild.removeAttribute('tabindex');
706 sb.removeAttribute('open');
707 sb.style.width = sb.style.height = '';
709 ul.classList.remove('dropdown');
710 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
713 fl.classList.remove('cbi-dropdown-open');
716 this.setFocus(sb, sb);
718 this.saveValues(sb, ul);
721 toggleItem: function(sb, li, force_state) {
722 if (li.hasAttribute('unselectable'))
725 if (this.options.multiple) {
726 var cbox = li.querySelector('input[type="checkbox"]'),
727 items = li.parentNode.querySelectorAll('li'),
728 label = sb.querySelector('ul.preview'),
729 sel = li.parentNode.querySelectorAll('[selected]').length,
730 more = sb.querySelector('.more'),
731 ndisplay = this.options.display_items,
734 if (li.hasAttribute('selected')) {
735 if (force_state !== true) {
736 if (sel > 1 || this.options.optional) {
737 li.removeAttribute('selected');
738 cbox.checked = cbox.disabled = false;
742 cbox.disabled = true;
747 if (force_state !== false) {
748 li.setAttribute('selected', '');
750 cbox.disabled = false;
755 while (label && label.firstElementChild)
756 label.removeChild(label.firstElementChild);
758 for (var i = 0; i < items.length; i++) {
759 items[i].removeAttribute('display');
760 if (items[i].hasAttribute('selected')) {
761 if (ndisplay-- > 0) {
762 items[i].setAttribute('display', n++);
764 label.appendChild(items[i].cloneNode(true));
766 var c = items[i].querySelector('input[type="checkbox"]');
768 c.disabled = (sel == 1 && !this.options.optional);
773 sb.setAttribute('more', '');
775 sb.removeAttribute('more');
777 if (ndisplay === this.options.display_items)
778 sb.setAttribute('empty', '');
780 sb.removeAttribute('empty');
782 L.dom.content(more, (ndisplay === this.options.display_items)
783 ? (this.options.select_placeholder || this.options.placeholder) : '···');
786 var sel = li.parentNode.querySelector('[selected]');
788 sel.removeAttribute('display');
789 sel.removeAttribute('selected');
792 li.setAttribute('display', 0);
793 li.setAttribute('selected', '');
795 this.closeDropdown(sb, true);
798 this.saveValues(sb, li.parentNode);
801 transformItem: function(sb, li) {
802 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
805 while (li.firstChild)
806 label.appendChild(li.firstChild);
808 li.appendChild(cbox);
809 li.appendChild(label);
812 saveValues: function(sb, ul) {
813 var sel = ul.querySelectorAll('li[selected]'),
814 div = sb.lastElementChild,
815 name = this.options.name,
819 while (div.lastElementChild)
820 div.removeChild(div.lastElementChild);
822 sel.forEach(function (s) {
823 if (s.hasAttribute('placeholder'))
828 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
832 div.appendChild(E('input', {
840 strval += strval.length ? ' ' + v.value : v.value;
848 if (this.options.multiple)
849 detail.values = values;
851 detail.value = values.length ? values[0] : null;
855 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
861 setValues: function(sb, values) {
862 var ul = sb.querySelector('ul');
864 if (this.options.create) {
865 for (var value in values) {
866 this.createItems(sb, value);
868 if (!this.options.multiple)
873 if (this.options.multiple) {
874 var lis = ul.querySelectorAll('li[data-value]');
875 for (var i = 0; i < lis.length; i++) {
876 var value = lis[i].getAttribute('data-value');
877 if (values === null || !(value in values))
878 this.toggleItem(sb, lis[i], false);
880 this.toggleItem(sb, lis[i], true);
884 var ph = ul.querySelector('li[placeholder]');
886 this.toggleItem(sb, ph);
888 var lis = ul.querySelectorAll('li[data-value]');
889 for (var i = 0; i < lis.length; i++) {
890 var value = lis[i].getAttribute('data-value');
891 if (values !== null && (value in values))
892 this.toggleItem(sb, lis[i]);
897 setFocus: function(sb, elem, scroll) {
898 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
901 if (sb.target && findParent(sb.target, 'ul.dropdown'))
904 document.querySelectorAll('.focus').forEach(function(e) {
905 if (!matchesElem(e, 'input')) {
906 e.classList.remove('focus');
913 elem.classList.add('focus');
916 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
920 createItems: function(sb, value) {
922 val = (value || '').trim(),
923 ul = sb.querySelector('ul');
925 if (!sbox.options.multiple)
926 val = val.length ? [ val ] : [];
928 val = val.length ? val.split(/\s+/) : [];
930 val.forEach(function(item) {
933 ul.childNodes.forEach(function(li) {
934 if (li.getAttribute && li.getAttribute('data-value') === item)
940 tpl = sb.querySelector(sbox.options.create_template);
943 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
945 markup = '<li data-value="{{value}}">{{value}}</li>';
947 new_item = E(markup.replace(/{{value}}/g, '%h'.format(item)));
949 if (sbox.options.multiple) {
950 sbox.transformItem(sb, new_item);
953 var old = ul.querySelector('li[created]');
957 new_item.setAttribute('created', '');
960 new_item = ul.insertBefore(new_item, ul.lastElementChild);
963 sbox.toggleItem(sb, new_item, true);
964 sbox.setFocus(sb, new_item, true);
968 closeAllDropdowns: function() {
969 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
970 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
974 handleClick: function(ev) {
975 var sb = ev.currentTarget;
977 if (!sb.hasAttribute('open')) {
978 if (!matchesElem(ev.target, 'input'))
979 this.openDropdown(sb);
982 var li = findParent(ev.target, 'li');
983 if (li && li.parentNode.classList.contains('dropdown'))
984 this.toggleItem(sb, li);
985 else if (li && li.parentNode.classList.contains('preview'))
986 this.closeDropdown(sb);
987 else if (matchesElem(ev.target, 'span.open, span.more'))
988 this.closeDropdown(sb);
992 ev.stopPropagation();
995 handleKeydown: function(ev) {
996 var sb = ev.currentTarget;
998 if (matchesElem(ev.target, 'input'))
1001 if (!sb.hasAttribute('open')) {
1002 switch (ev.keyCode) {
1007 this.openDropdown(sb);
1008 ev.preventDefault();
1012 var active = findParent(document.activeElement, 'li');
1014 switch (ev.keyCode) {
1016 this.closeDropdown(sb);
1021 if (!active.hasAttribute('selected'))
1022 this.toggleItem(sb, active);
1023 this.closeDropdown(sb);
1024 ev.preventDefault();
1030 this.toggleItem(sb, active);
1031 ev.preventDefault();
1036 if (active && active.previousElementSibling) {
1037 this.setFocus(sb, active.previousElementSibling);
1038 ev.preventDefault();
1043 if (active && active.nextElementSibling) {
1044 this.setFocus(sb, active.nextElementSibling);
1045 ev.preventDefault();
1052 handleDropdownClose: function(ev) {
1053 var sb = ev.currentTarget;
1055 this.closeDropdown(sb, true);
1058 handleDropdownSelect: function(ev) {
1059 var sb = ev.currentTarget,
1060 li = findParent(ev.target, 'li');
1065 this.toggleItem(sb, li);
1066 this.closeDropdown(sb, true);
1069 handleMouseover: function(ev) {
1070 var sb = ev.currentTarget;
1072 if (!sb.hasAttribute('open'))
1075 var li = findParent(ev.target, 'li');
1077 if (li && li.parentNode.classList.contains('dropdown'))
1078 this.setFocus(sb, li);
1081 handleFocus: function(ev) {
1082 var sb = ev.currentTarget;
1084 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
1085 if (s !== sb || sb.hasAttribute('open'))
1086 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1090 handleCanaryFocus: function(ev) {
1091 this.closeDropdown(ev.currentTarget.parentNode);
1094 handleCreateKeydown: function(ev) {
1095 var input = ev.currentTarget,
1096 sb = findParent(input, '.cbi-dropdown');
1098 switch (ev.keyCode) {
1100 ev.preventDefault();
1102 if (input.classList.contains('cbi-input-invalid'))
1105 this.createItems(sb, input.value);
1112 handleCreateFocus: function(ev) {
1113 var input = ev.currentTarget,
1114 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1115 sb = findParent(input, '.cbi-dropdown');
1118 cbox.checked = true;
1120 sb.setAttribute('locked-in', '');
1123 handleCreateBlur: function(ev) {
1124 var input = ev.currentTarget,
1125 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
1126 sb = findParent(input, '.cbi-dropdown');
1129 cbox.checked = false;
1131 sb.removeAttribute('locked-in');
1134 handleCreateClick: function(ev) {
1135 ev.currentTarget.querySelector(this.options.create_query).focus();
1138 setValue: function(values) {
1139 if (this.options.multiple) {
1140 if (!Array.isArray(values))
1141 values = (values != null && values != '') ? [ values ] : [];
1145 for (var i = 0; i < values.length; i++)
1146 v[values[i]] = true;
1148 this.setValues(this.node, v);
1153 if (values != null) {
1154 if (Array.isArray(values))
1155 v[values[0]] = true;
1160 this.setValues(this.node, v);
1164 getValue: function() {
1165 var div = this.node.lastElementChild,
1166 h = div.querySelectorAll('input[type="hidden"]'),
1169 for (var i = 0; i < h.length; i++)
1172 return this.options.multiple ? v : v[0];
1176 var UICombobox = UIDropdown.extend({
1177 __init__: function(value, choices, options) {
1178 this.super('__init__', [ value, choices, Object.assign({
1179 select_placeholder: _('-- Please choose --'),
1180 custom_placeholder: _('-- custom --'),
1191 var UIComboButton = UIDropdown.extend({
1192 __init__: function(value, choices, options) {
1193 this.super('__init__', [ value, choices, Object.assign({
1202 render: function(/* ... */) {
1203 var node = UIDropdown.prototype.render.apply(this, arguments),
1204 val = this.getValue();
1206 if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
1207 node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
1212 handleClick: function(ev) {
1213 var sb = ev.currentTarget,
1216 if (sb.hasAttribute('open') || L.dom.matches(t, '.cbi-dropdown > span.open'))
1217 return UIDropdown.prototype.handleClick.apply(this, arguments);
1219 if (this.options.click)
1220 return this.options.click.call(sb, ev, this.getValue());
1223 toggleItem: function(sb /*, ... */) {
1224 var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
1225 val = this.getValue();
1227 if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
1228 sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
1230 sb.setAttribute('class', 'cbi-dropdown');
1236 var UIDynamicList = UIElement.extend({
1237 __init__: function(values, choices, options) {
1238 if (!Array.isArray(values))
1239 values = (values != null && values != '') ? [ values ] : [];
1241 if (typeof(choices) != 'object')
1244 this.values = values;
1245 this.choices = choices;
1246 this.options = Object.assign({}, options, {
1252 render: function() {
1254 'id': this.options.id,
1255 'class': 'cbi-dynlist'
1256 }, E('div', { 'class': 'add-item' }));
1259 var cbox = new UICombobox(null, this.choices, this.options);
1260 dl.lastElementChild.appendChild(cbox.render());
1263 var inputEl = E('input', {
1264 'id': this.options.id ? 'widget.' + this.options.id : null,
1266 'class': 'cbi-input-text',
1267 'placeholder': this.options.placeholder
1270 dl.lastElementChild.appendChild(inputEl);
1271 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1273 if (this.options.datatype)
1274 L.ui.addValidator(inputEl, this.options.datatype,
1275 true, null, 'blur', 'keyup');
1278 for (var i = 0; i < this.values.length; i++)
1279 this.addItem(dl, this.values[i],
1280 this.choices ? this.choices[this.values[i]] : null);
1282 return this.bind(dl);
1285 bind: function(dl) {
1286 dl.addEventListener('click', L.bind(this.handleClick, this));
1287 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
1288 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
1292 this.setUpdateEvents(dl, 'cbi-dynlist-change');
1293 this.setChangeEvents(dl, 'cbi-dynlist-change');
1295 L.dom.bindClassInstance(dl, this);
1300 addItem: function(dl, value, text, flash) {
1302 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
1303 E('span', {}, [ text || value ]),
1306 'name': this.options.name,
1307 'value': value })]);
1309 dl.querySelectorAll('.item').forEach(function(item) {
1313 var hidden = item.querySelector('input[type="hidden"]');
1315 if (hidden && hidden.parentNode !== item)
1318 if (hidden && hidden.value === value)
1323 var ai = dl.querySelector('.add-item');
1324 ai.parentNode.insertBefore(new_item, ai);
1327 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1338 removeItem: function(dl, item) {
1339 var value = item.querySelector('input[type="hidden"]').value;
1340 var sb = dl.querySelector('.cbi-dropdown');
1342 sb.querySelectorAll('ul > li').forEach(function(li) {
1343 if (li.getAttribute('data-value') === value) {
1344 if (li.hasAttribute('dynlistcustom'))
1345 li.parentNode.removeChild(li);
1347 li.removeAttribute('unselectable');
1351 item.parentNode.removeChild(item);
1353 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1364 handleClick: function(ev) {
1365 var dl = ev.currentTarget,
1366 item = findParent(ev.target, '.item');
1369 this.removeItem(dl, item);
1371 else if (matchesElem(ev.target, '.cbi-button-add')) {
1372 var input = ev.target.previousElementSibling;
1373 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
1374 this.addItem(dl, input.value, null, true);
1380 handleDropdownChange: function(ev) {
1381 var dl = ev.currentTarget,
1382 sbIn = ev.detail.instance,
1383 sbEl = ev.detail.element,
1384 sbVal = ev.detail.value;
1389 sbIn.setValues(sbEl, null);
1390 sbVal.element.setAttribute('unselectable', '');
1392 if (sbVal.element.hasAttribute('created')) {
1393 sbVal.element.removeAttribute('created');
1394 sbVal.element.setAttribute('dynlistcustom', '');
1397 this.addItem(dl, sbVal.value, sbVal.text, true);
1400 handleKeydown: function(ev) {
1401 var dl = ev.currentTarget,
1402 item = findParent(ev.target, '.item');
1405 switch (ev.keyCode) {
1406 case 8: /* backspace */
1407 if (item.previousElementSibling)
1408 item.previousElementSibling.focus();
1410 this.removeItem(dl, item);
1413 case 46: /* delete */
1414 if (item.nextElementSibling) {
1415 if (item.nextElementSibling.classList.contains('item'))
1416 item.nextElementSibling.focus();
1418 item.nextElementSibling.firstElementChild.focus();
1421 this.removeItem(dl, item);
1425 else if (matchesElem(ev.target, '.cbi-input-text')) {
1426 switch (ev.keyCode) {
1427 case 13: /* enter */
1428 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
1429 this.addItem(dl, ev.target.value, null, true);
1430 ev.target.value = '';
1435 ev.preventDefault();
1441 getValue: function() {
1442 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1443 input = this.node.querySelector('.add-item > input[type="text"]'),
1446 for (var i = 0; i < items.length; i++)
1447 v.push(items[i].value);
1449 if (input && input.value != null && input.value.match(/\S/) &&
1450 input.classList.contains('cbi-input-invalid') == false &&
1451 v.filter(function(s) { return s == input.value }).length == 0)
1452 v.push(input.value);
1457 setValue: function(values) {
1458 if (!Array.isArray(values))
1459 values = (values != null && values != '') ? [ values ] : [];
1461 var items = this.node.querySelectorAll('.item');
1463 for (var i = 0; i < items.length; i++)
1464 if (items[i].parentNode === this.node)
1465 this.removeItem(this.node, items[i]);
1467 for (var i = 0; i < values.length; i++)
1468 this.addItem(this.node, values[i],
1469 this.choices ? this.choices[values[i]] : null);
1473 var UIHiddenfield = UIElement.extend({
1474 __init__: function(value, options) {
1476 this.options = Object.assign({
1481 render: function() {
1482 var hiddenEl = E('input', {
1483 'id': this.options.id,
1488 return this.bind(hiddenEl);
1491 bind: function(hiddenEl) {
1492 this.node = hiddenEl;
1494 L.dom.bindClassInstance(hiddenEl, this);
1499 getValue: function() {
1500 return this.node.value;
1503 setValue: function(value) {
1504 this.node.value = value;
1508 var UIFileUpload = UIElement.extend({
1509 __init__: function(value, options) {
1511 this.options = Object.assign({
1513 enable_upload: true,
1514 enable_remove: true,
1515 root_directory: '/etc/luci-uploads'
1519 bind: function(browserEl) {
1520 this.node = browserEl;
1522 this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1523 this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1525 L.dom.bindClassInstance(browserEl, this);
1530 render: function() {
1531 return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
1534 if (L.isObject(stat) && stat.type != 'directory')
1537 if (this.stat != null)
1538 label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
1539 else if (this.value != null)
1540 label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
1542 label = [ _('Select file…') ];
1544 return this.bind(E('div', { 'id': this.options.id }, [
1547 'click': L.ui.createHandlerFn(this, 'handleFileBrowser')
1550 'class': 'cbi-filebrowser'
1554 'name': this.options.name,
1561 truncatePath: function(path) {
1562 if (path.length > 50)
1563 path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
1568 iconForType: function(type) {
1572 'src': L.resource('cbi/link.gif'),
1573 'title': _('Symbolic link'),
1579 'src': L.resource('cbi/folder.gif'),
1580 'title': _('Directory'),
1586 'src': L.resource('cbi/file.gif'),
1593 canonicalizePath: function(path) {
1594 return path.replace(/\/{2,}/, '/')
1595 .replace(/\/\.(\/|$)/g, '/')
1596 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1597 .replace(/\/$/, '');
1600 splitPath: function(path) {
1601 var croot = this.canonicalizePath(this.options.root_directory || '/'),
1602 cpath = this.canonicalizePath(path || '/');
1604 if (cpath.length <= croot.length)
1607 if (cpath.charAt(croot.length) != '/')
1610 var parts = cpath.substring(croot.length + 1).split(/\//);
1612 parts.unshift(croot);
1617 handleUpload: function(path, list, ev) {
1618 var form = ev.target.parentNode,
1619 fileinput = form.querySelector('input[type="file"]'),
1620 nameinput = form.querySelector('input[type="text"]'),
1621 filename = (nameinput.value != null ? nameinput.value : '').trim();
1623 ev.preventDefault();
1625 if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
1628 var existing = list.filter(function(e) { return e.name == filename })[0];
1630 if (existing != null && existing.type == 'directory')
1631 return alert(_('A directory with the same name already exists.'));
1632 else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
1635 var data = new FormData();
1637 data.append('sessionid', L.env.sessionid);
1638 data.append('filename', path + '/' + filename);
1639 data.append('filedata', fileinput.files[0]);
1641 return L.Request.post('/cgi-bin/cgi-upload', data, {
1642 progress: L.bind(function(btn, ev) {
1643 btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
1645 }).then(L.bind(function(path, ev, res) {
1646 var reply = res.json();
1648 if (L.isObject(reply) && reply.failure)
1649 alert(_('Upload request failed: %s').format(reply.message));
1651 return this.handleSelect(path, null, ev);
1652 }, this, path, ev));
1655 handleDelete: function(path, fileStat, ev) {
1656 var parent = path.replace(/\/[^\/]+$/, '') || '/',
1657 name = path.replace(/^.+\//, ''),
1660 ev.preventDefault();
1662 if (fileStat.type == 'directory')
1663 msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
1665 msg = _('Do you really want to delete "%s" ?').format(name);
1668 var button = this.node.firstElementChild,
1669 hidden = this.node.lastElementChild;
1671 if (path == hidden.value) {
1672 L.dom.content(button, _('Select file…'));
1676 return fs.remove(path).then(L.bind(function(parent, ev) {
1677 return this.handleSelect(parent, null, ev);
1678 }, this, parent, ev)).catch(function(err) {
1679 alert(_('Delete request failed: %s').format(err.message));
1684 renderUpload: function(path, list) {
1685 if (!this.options.enable_upload)
1691 'class': 'btn cbi-button-positive',
1692 'click': function(ev) {
1693 var uploadForm = ev.target.nextElementSibling,
1694 fileInput = uploadForm.querySelector('input[type="file"]');
1696 ev.target.style.display = 'none';
1697 uploadForm.style.display = '';
1700 }, _('Upload file…')),
1701 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1704 'style': 'display:none',
1705 'change': function(ev) {
1706 var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
1707 uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
1709 nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
1710 uploadbtn.disabled = false;
1715 'click': function(ev) {
1716 ev.preventDefault();
1717 ev.target.previousElementSibling.click();
1719 }, [ _('Browse…') ]),
1720 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1722 'class': 'btn cbi-button-save',
1723 'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
1725 }, [ _('Upload file') ])
1730 renderListing: function(container, path, list) {
1731 var breadcrumb = E('p'),
1734 list.sort(function(a, b) {
1735 var isDirA = (a.type == 'directory'),
1736 isDirB = (b.type == 'directory');
1738 if (isDirA != isDirB)
1739 return isDirA < isDirB;
1741 return a.name > b.name;
1744 for (var i = 0; i < list.length; i++) {
1745 if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
1748 var entrypath = this.canonicalizePath(path + '/' + list[i].name),
1749 selected = (entrypath == this.node.lastElementChild.value),
1750 mtime = new Date(list[i].mtime * 1000);
1752 rows.appendChild(E('li', [
1753 E('div', { 'class': 'name' }, [
1754 this.iconForType(list[i].type),
1758 'style': selected ? 'font-weight:bold' : null,
1759 'click': L.ui.createHandlerFn(this, 'handleSelect',
1760 entrypath, list[i].type != 'directory' ? list[i] : null)
1761 }, '%h'.format(list[i].name))
1763 E('div', { 'class': 'mtime hide-xs' }, [
1764 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1765 mtime.getFullYear(),
1766 mtime.getMonth() + 1,
1773 selected ? E('button', {
1775 'click': L.ui.createHandlerFn(this, 'handleReset')
1776 }, [ _('Deselect') ]) : '',
1777 this.options.enable_remove ? E('button', {
1778 'class': 'btn cbi-button-negative',
1779 'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
1780 }, [ _('Delete') ]) : ''
1785 if (!rows.firstElementChild)
1786 rows.appendChild(E('em', _('No entries in this directory')));
1788 var dirs = this.splitPath(path),
1791 for (var i = 0; i < dirs.length; i++) {
1792 cur = cur ? cur + '/' + dirs[i] : dirs[i];
1793 L.dom.append(breadcrumb, [
1797 'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
1798 }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
1802 L.dom.content(container, [
1805 E('div', { 'class': 'right' }, [
1806 this.renderUpload(path, list),
1810 'click': L.ui.createHandlerFn(this, 'handleCancel')
1816 handleCancel: function(ev) {
1817 var button = this.node.firstElementChild,
1818 browser = button.nextElementSibling;
1820 browser.classList.remove('open');
1821 button.style.display = '';
1823 this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1826 handleReset: function(ev) {
1827 var button = this.node.firstElementChild,
1828 hidden = this.node.lastElementChild;
1831 L.dom.content(button, _('Select file…'));
1833 this.handleCancel(ev);
1836 handleSelect: function(path, fileStat, ev) {
1837 var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
1838 ul = browser.querySelector('ul');
1840 if (fileStat == null) {
1841 L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1842 L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
1845 var button = this.node.firstElementChild,
1846 hidden = this.node.lastElementChild;
1848 path = this.canonicalizePath(path);
1850 L.dom.content(button, [
1851 this.iconForType(fileStat.type),
1852 ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
1855 browser.classList.remove('open');
1856 button.style.display = '';
1857 hidden.value = path;
1859 this.stat = Object.assign({ path: path }, fileStat);
1860 this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
1864 handleFileBrowser: function(ev) {
1865 var button = ev.target,
1866 browser = button.nextElementSibling,
1867 path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : this.options.root_directory;
1869 if (this.options.root_directory.indexOf(path) != 0)
1870 path = this.options.root_directory;
1872 ev.preventDefault();
1874 return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
1875 document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
1876 L.dom.findClassInstance(browserEl).handleCancel(ev);
1879 button.style.display = 'none';
1880 browser.classList.add('open');
1882 return this.renderListing(browser, path, list);
1883 }, this, button, browser, path));
1886 getValue: function() {
1887 return this.node.lastElementChild.value;
1890 setValue: function(value) {
1891 this.node.lastElementChild.value = value;
1896 return L.Class.extend({
1897 __init__: function() {
1898 modalDiv = document.body.appendChild(
1899 L.dom.create('div', { id: 'modal_overlay' },
1900 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1902 tooltipDiv = document.body.appendChild(
1903 L.dom.create('div', { class: 'cbi-tooltip' }));
1905 /* setup old aliases */
1906 L.showModal = this.showModal;
1907 L.hideModal = this.hideModal;
1908 L.showTooltip = this.showTooltip;
1909 L.hideTooltip = this.hideTooltip;
1910 L.itemlist = this.itemlist;
1912 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1913 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1914 document.addEventListener('focus', this.showTooltip.bind(this), true);
1915 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1917 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1918 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
1919 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
1923 showModal: function(title, children /* , ... */) {
1924 var dlg = modalDiv.firstElementChild;
1926 dlg.setAttribute('class', 'modal');
1928 for (var i = 2; i < arguments.length; i++)
1929 dlg.classList.add(arguments[i]);
1931 L.dom.content(dlg, L.dom.create('h4', {}, title));
1932 L.dom.append(dlg, children);
1934 document.body.classList.add('modal-overlay-active');
1939 hideModal: function() {
1940 document.body.classList.remove('modal-overlay-active');
1944 showTooltip: function(ev) {
1945 var target = findParent(ev.target, '[data-tooltip]');
1950 if (tooltipTimeout !== null) {
1951 window.clearTimeout(tooltipTimeout);
1952 tooltipTimeout = null;
1955 var rect = target.getBoundingClientRect(),
1956 x = rect.left + window.pageXOffset,
1957 y = rect.top + rect.height + window.pageYOffset;
1959 tooltipDiv.className = 'cbi-tooltip';
1960 tooltipDiv.innerHTML = '▲ ';
1961 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1963 if (target.hasAttribute('data-tooltip-style'))
1964 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1966 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1967 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1968 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1971 tooltipDiv.style.top = y + 'px';
1972 tooltipDiv.style.left = x + 'px';
1973 tooltipDiv.style.opacity = 1;
1975 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1977 detail: { target: target }
1981 hideTooltip: function(ev) {
1982 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1983 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1986 if (tooltipTimeout !== null) {
1987 window.clearTimeout(tooltipTimeout);
1988 tooltipTimeout = null;
1991 tooltipDiv.style.opacity = 0;
1992 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1994 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1997 addNotification: function(title, children /*, ... */) {
1998 var mc = document.querySelector('#maincontent') || document.body;
1999 var msg = E('div', {
2000 'class': 'alert-message fade-in',
2001 'style': 'display:flex',
2002 'transitionend': function(ev) {
2003 var node = ev.currentTarget;
2004 if (node.parentNode && node.classList.contains('fade-out'))
2005 node.parentNode.removeChild(node);
2008 E('div', { 'style': 'flex:10' }),
2009 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
2012 'style': 'margin-left:auto; margin-top:auto',
2013 'click': function(ev) {
2014 L.dom.parent(ev.target, '.alert-message').classList.add('fade-out');
2017 }, [ _('Dismiss') ])
2022 L.dom.append(msg.firstElementChild, E('h4', {}, title));
2024 L.dom.append(msg.firstElementChild, children);
2026 for (var i = 2; i < arguments.length; i++)
2027 msg.classList.add(arguments[i]);
2029 mc.insertBefore(msg, mc.firstElementChild);
2035 itemlist: function(node, items, separators) {
2038 if (!Array.isArray(separators))
2039 separators = [ separators || E('br') ];
2041 for (var i = 0; i < items.length; i += 2) {
2042 if (items[i+1] !== null && items[i+1] !== undefined) {
2043 var sep = separators[(i/2) % separators.length],
2046 children.push(E('span', { class: 'nowrap' }, [
2047 items[i] ? E('strong', items[i] + ': ') : '',
2051 if ((i+2) < items.length)
2052 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
2056 L.dom.content(node, children);
2062 tabs: L.Class.singleton({
2064 var groups = [], prevGroup = null, currGroup = null;
2066 document.querySelectorAll('[data-tab]').forEach(function(tab) {
2067 var parent = tab.parentNode;
2069 if (L.dom.matches(tab, 'li') && L.dom.matches(parent, 'ul.cbi-tabmenu'))
2072 if (!parent.hasAttribute('data-tab-group'))
2073 parent.setAttribute('data-tab-group', groups.length);
2075 currGroup = +parent.getAttribute('data-tab-group');
2077 if (currGroup !== prevGroup) {
2078 prevGroup = currGroup;
2080 if (!groups[currGroup])
2081 groups[currGroup] = [];
2084 groups[currGroup].push(tab);
2087 for (var i = 0; i < groups.length; i++)
2088 this.initTabGroup(groups[i]);
2090 document.addEventListener('dependency-update', this.updateTabs.bind(this));
2095 initTabGroup: function(panes) {
2096 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
2099 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
2100 group = panes[0].parentNode,
2101 groupId = +group.getAttribute('data-tab-group'),
2104 if (group.getAttribute('data-initialized') === 'true')
2107 for (var i = 0, pane; pane = panes[i]; i++) {
2108 var name = pane.getAttribute('data-tab'),
2109 title = pane.getAttribute('data-tab-title'),
2110 active = pane.getAttribute('data-tab-active') === 'true';
2112 menu.appendChild(E('li', {
2113 'style': this.isEmptyPane(pane) ? 'display:none' : null,
2114 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
2118 'click': this.switchTab.bind(this)
2125 group.parentNode.insertBefore(menu, group);
2126 group.setAttribute('data-initialized', true);
2128 if (selected === null) {
2129 selected = this.getActiveTabId(panes[0]);
2131 if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
2132 for (var i = 0; i < panes.length; i++) {
2133 if (!this.isEmptyPane(panes[i])) {
2140 menu.childNodes[selected].classList.add('cbi-tab');
2141 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
2142 panes[selected].setAttribute('data-tab-active', 'true');
2144 this.setActiveTabId(panes[selected], selected);
2147 this.updateTabs(group);
2150 isEmptyPane: function(pane) {
2151 return L.dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
2154 getPathForPane: function(pane) {
2155 var path = [], node = null;
2157 for (node = pane ? pane.parentNode : null;
2158 node != null && node.hasAttribute != null;
2159 node = node.parentNode)
2161 if (node.hasAttribute('data-tab'))
2162 path.unshift(node.getAttribute('data-tab'));
2163 else if (node.hasAttribute('data-section-id'))
2164 path.unshift(node.getAttribute('data-section-id'));
2167 return path.join('/');
2170 getActiveTabState: function() {
2171 var page = document.body.getAttribute('data-page');
2174 var val = JSON.parse(window.sessionStorage.getItem('tab'));
2175 if (val.page === page && L.isObject(val.paths))
2180 window.sessionStorage.removeItem('tab');
2181 return { page: page, paths: {} };
2184 getActiveTabId: function(pane) {
2185 var path = this.getPathForPane(pane);
2186 return +this.getActiveTabState().paths[path] || 0;
2189 setActiveTabId: function(pane, tabIndex) {
2190 var path = this.getPathForPane(pane);
2193 var state = this.getActiveTabState();
2194 state.paths[path] = tabIndex;
2196 window.sessionStorage.setItem('tab', JSON.stringify(state));
2198 catch (e) { return false; }
2203 updateTabs: function(ev, root) {
2204 (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
2205 var menu = pane.parentNode.previousElementSibling,
2206 tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
2207 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
2212 if (this.isEmptyPane(pane)) {
2213 tab.style.display = 'none';
2214 tab.classList.remove('flash');
2216 else if (tab.style.display === 'none') {
2217 tab.style.display = '';
2218 requestAnimationFrame(function() { tab.classList.add('flash') });
2222 tab.setAttribute('data-errors', n_errors);
2223 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
2224 tab.setAttribute('data-tooltip-style', 'error');
2227 tab.removeAttribute('data-errors');
2228 tab.removeAttribute('data-tooltip');
2233 switchTab: function(ev) {
2234 var tab = ev.target.parentNode,
2235 name = tab.getAttribute('data-tab'),
2236 menu = tab.parentNode,
2237 group = menu.nextElementSibling,
2238 groupId = +group.getAttribute('data-tab-group'),
2241 ev.preventDefault();
2243 if (!tab.classList.contains('cbi-tab-disabled'))
2246 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
2247 tab.classList.remove('cbi-tab');
2248 tab.classList.remove('cbi-tab-disabled');
2250 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
2253 group.childNodes.forEach(function(pane) {
2254 if (L.dom.matches(pane, '[data-tab]')) {
2255 if (pane.getAttribute('data-tab') === name) {
2256 pane.setAttribute('data-tab-active', 'true');
2257 L.ui.tabs.setActiveTabId(pane, index);
2260 pane.setAttribute('data-tab-active', 'false');
2269 /* File uploading */
2270 uploadFile: function(path, progressStatusNode) {
2271 return new Promise(function(resolveFn, rejectFn) {
2272 L.ui.showModal(_('Uploading file…'), [
2273 E('p', _('Please select the file to upload.')),
2274 E('div', { 'style': 'display:flex' }, [
2275 E('div', { 'class': 'left', 'style': 'flex:1' }, [
2278 style: 'display:none',
2279 change: function(ev) {
2280 var modal = L.dom.parent(ev.target, '.modal'),
2281 body = modal.querySelector('p'),
2282 upload = modal.querySelector('.cbi-button-action.important'),
2283 file = ev.currentTarget.files[0];
2288 L.dom.content(body, [
2290 E('li', {}, [ '%s: %s'.format(_('Name'), file.name.replace(/^.*[\\\/]/, '')) ]),
2291 E('li', {}, [ '%s: %1024mB'.format(_('Size'), file.size) ])
2295 upload.disabled = false;
2301 'click': function(ev) {
2302 ev.target.previousElementSibling.click();
2304 }, [ _('Browse…') ])
2306 E('div', { 'class': 'right', 'style': 'flex:1' }, [
2309 'click': function() {
2311 rejectFn(new Error('Upload has been cancelled'));
2313 }, [ _('Cancel') ]),
2316 'class': 'btn cbi-button-action important',
2318 'click': function(ev) {
2319 var input = L.dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
2321 if (!input.files[0])
2324 var progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
2326 L.ui.showModal(_('Uploading file…'), [ progress ]);
2328 var data = new FormData();
2330 data.append('sessionid', rpc.getSessionID());
2331 data.append('filename', path);
2332 data.append('filedata', input.files[0]);
2334 var filename = input.files[0].name;
2336 L.Request.post('/cgi-bin/cgi-upload', data, {
2338 progress: function(pev) {
2339 var percent = (pev.loaded / pev.total) * 100;
2341 if (progressStatusNode)
2342 progressStatusNode.data = '%.2f%%'.format(percent);
2344 progress.setAttribute('title', '%.2f%%'.format(percent));
2345 progress.firstElementChild.style.width = '%.2f%%'.format(percent);
2347 }).then(function(res) {
2348 var reply = res.json();
2352 if (L.isObject(reply) && reply.failure) {
2353 L.ui.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
2354 rejectFn(new Error(reply.failure));
2357 reply.name = filename;
2372 /* Reconnect handling */
2373 pingDevice: function(proto, ipaddr) {
2374 var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
2376 return new Promise(function(resolveFn, rejectFn) {
2377 var img = new Image();
2379 img.onload = resolveFn;
2380 img.onerror = rejectFn;
2382 window.setTimeout(rejectFn, 1000);
2388 awaitReconnect: function(/* ... */) {
2389 var ipaddrs = arguments.length ? arguments : [ window.location.host ];
2391 window.setTimeout(L.bind(function() {
2392 L.Poll.add(L.bind(function() {
2393 var tasks = [], reachable = false;
2395 for (var i = 0; i < 2; i++)
2396 for (var j = 0; j < ipaddrs.length; j++)
2397 tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
2398 .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
2400 return Promise.all(tasks).then(function() {
2403 window.location = reachable;
2411 changes: L.Class.singleton({
2413 if (!L.env.sessionid)
2416 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
2419 setIndicator: function(n) {
2420 var i = document.querySelector('.uci_change_indicator');
2422 var poll = document.getElementById('xhr_poll_status');
2423 i = poll.parentNode.insertBefore(E('a', {
2425 'class': 'uci_change_indicator label notice',
2426 'click': L.bind(this.displayChanges, this)
2431 L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
2432 i.classList.add('flash');
2433 i.style.display = '';
2436 i.classList.remove('flash');
2437 i.style.display = 'none';
2441 renderChangeIndicator: function(changes) {
2444 for (var config in changes)
2445 if (changes.hasOwnProperty(config))
2446 n_changes += changes[config].length;
2448 this.changes = changes;
2449 this.setIndicator(n_changes);
2453 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2454 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2455 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2456 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2457 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2458 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2459 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2460 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2461 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2462 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2465 displayChanges: function() {
2466 var list = E('div', { 'class': 'uci-change-list' }),
2467 dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
2468 E('div', { 'class': 'cbi-section' }, [
2469 E('strong', _('Legend:')),
2470 E('div', { 'class': 'uci-change-legend' }, [
2471 E('div', { 'class': 'uci-change-legend-label' }, [
2472 E('ins', ' '), ' ', _('Section added') ]),
2473 E('div', { 'class': 'uci-change-legend-label' }, [
2474 E('del', ' '), ' ', _('Section removed') ]),
2475 E('div', { 'class': 'uci-change-legend-label' }, [
2476 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2477 E('div', { 'class': 'uci-change-legend-label' }, [
2478 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2480 E('div', { 'class': 'right' }, [
2483 'click': L.ui.hideModal
2484 }, [ _('Dismiss') ]), ' ',
2486 'class': 'cbi-button cbi-button-positive important',
2487 'click': L.bind(this.apply, this, true)
2488 }, [ _('Save & Apply') ]), ' ',
2490 'class': 'cbi-button cbi-button-reset',
2491 'click': L.bind(this.revert, this)
2492 }, [ _('Revert') ])])])
2495 for (var config in this.changes) {
2496 if (!this.changes.hasOwnProperty(config))
2499 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
2501 for (var i = 0, added = null; i < this.changes[config].length; i++) {
2502 var chg = this.changes[config][i],
2503 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
2505 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
2511 if (added != null && chg[1] == added[0])
2512 return '@' + added[1] + '[-1]';
2517 return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
2524 if (chg[0] == 'add')
2525 added = [ chg[1], chg[2] ];
2529 list.appendChild(E('br'));
2530 dlg.classList.add('uci-dialog');
2533 displayStatus: function(type, content) {
2535 var message = L.ui.showModal('', '');
2537 message.classList.add('alert-message');
2538 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2541 L.dom.content(message, content);
2543 if (!this.was_polling) {
2544 this.was_polling = L.Request.poll.active();
2545 L.Request.poll.stop();
2551 if (this.was_polling)
2552 L.Request.poll.start();
2556 rollback: function(checked) {
2558 this.displayStatus('warning spinning',
2559 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2560 .format(L.env.apply_rollback)));
2562 var call = function(r, data, duration) {
2563 if (r.status === 204) {
2564 L.ui.changes.displayStatus('warning', [
2565 E('h4', _('Configuration changes have been rolled back!')),
2566 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)),
2567 E('div', { 'class': 'right' }, [
2570 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2571 }, [ _('Dismiss') ]), ' ',
2573 'class': 'btn cbi-button-action important',
2574 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2575 }, [ _('Revert changes') ]), ' ',
2577 'class': 'btn cbi-button-negative important',
2578 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2579 }, [ _('Apply unchecked') ])
2586 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2587 window.setTimeout(function() {
2588 L.Request.request(L.url('admin/uci/confirm'), {
2590 timeout: L.env.apply_timeout * 1000,
2591 query: { sid: L.env.sessionid, token: L.env.token }
2596 call({ status: 0 });
2599 this.displayStatus('warning', [
2600 E('h4', _('Device unreachable!')),
2601 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.'))
2606 confirm: function(checked, deadline, override_token) {
2608 var ts = Date.now();
2610 this.displayStatus('notice');
2613 this.confirm_auth = { token: override_token };
2615 var call = function(r, data, duration) {
2616 if (Date.now() >= deadline) {
2617 window.clearTimeout(tt);
2618 L.ui.changes.rollback(checked);
2621 else if (r && (r.status === 200 || r.status === 204)) {
2622 document.dispatchEvent(new CustomEvent('uci-applied'));
2624 L.ui.changes.setIndicator(0);
2625 L.ui.changes.displayStatus('notice',
2626 E('p', _('Configuration changes applied.')));
2628 window.clearTimeout(tt);
2629 window.setTimeout(function() {
2630 //L.ui.changes.displayStatus(false);
2631 window.location = window.location.href.split('#')[0];
2632 }, L.env.apply_display * 1000);
2637 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2638 window.setTimeout(function() {
2639 L.Request.request(L.url('admin/uci/confirm'), {
2641 timeout: L.env.apply_timeout * 1000,
2642 query: L.ui.changes.confirm_auth
2643 }).then(call, call);
2647 var tick = function() {
2648 var now = Date.now();
2650 L.ui.changes.displayStatus('notice spinning',
2651 E('p', _('Applying configuration changes… %ds')
2652 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2654 if (now >= deadline)
2657 tt = window.setTimeout(tick, 1000 - (now - ts));
2663 /* wait a few seconds for the settings to become effective */
2664 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2667 apply: function(checked) {
2668 this.displayStatus('notice spinning',
2669 E('p', _('Starting configuration apply…')));
2671 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2673 query: { sid: L.env.sessionid, token: L.env.token }
2674 }).then(function(r) {
2675 if (r.status === (checked ? 200 : 204)) {
2676 var tok = null; try { tok = r.json(); } catch(e) {}
2677 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2678 L.ui.changes.confirm_auth = tok;
2680 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2682 else if (checked && r.status === 204) {
2683 L.ui.changes.displayStatus('notice',
2684 E('p', _('There are no changes to apply')));
2686 window.setTimeout(function() {
2687 L.ui.changes.displayStatus(false);
2688 }, L.env.apply_display * 1000);
2691 L.ui.changes.displayStatus('warning',
2692 E('p', _('Apply request failed with status <code>%h</code>')
2693 .format(r.responseText || r.statusText || r.status)));
2695 window.setTimeout(function() {
2696 L.ui.changes.displayStatus(false);
2697 }, L.env.apply_display * 1000);
2702 revert: function() {
2703 this.displayStatus('notice spinning',
2704 E('p', _('Reverting configuration…')));
2706 L.Request.request(L.url('admin/uci/revert'), {
2708 query: { sid: L.env.sessionid, token: L.env.token }
2709 }).then(function(r) {
2710 if (r.status === 200) {
2711 document.dispatchEvent(new CustomEvent('uci-reverted'));
2713 L.ui.changes.setIndicator(0);
2714 L.ui.changes.displayStatus('notice',
2715 E('p', _('Changes have been reverted.')));
2717 window.setTimeout(function() {
2718 //L.ui.changes.displayStatus(false);
2719 window.location = window.location.href.split('#')[0];
2720 }, L.env.apply_display * 1000);
2723 L.ui.changes.displayStatus('warning',
2724 E('p', _('Revert request failed with status <code>%h</code>')
2725 .format(r.statusText || r.status)));
2727 window.setTimeout(function() {
2728 L.ui.changes.displayStatus(false);
2729 }, L.env.apply_display * 1000);
2735 addValidator: function(field, type, optional, vfunc /*, ... */) {
2739 var events = this.varargs(arguments, 3);
2740 if (events.length == 0)
2741 events.push('blur', 'keyup');
2744 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2745 validatorFn = cbiValidator.validate.bind(cbiValidator);
2747 for (var i = 0; i < events.length; i++)
2748 field.addEventListener(events[i], validatorFn);
2757 createHandlerFn: function(ctx, fn /*, ... */) {
2758 if (typeof(fn) == 'string')
2761 if (typeof(fn) != 'function')
2764 var arg_offset = arguments.length - 2;
2766 return Function.prototype.bind.apply(function() {
2767 var t = arguments[arg_offset].target;
2769 t.classList.add('spinning');
2775 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2776 t.classList.remove('spinning');
2779 }, this.varargs(arguments, 2, ctx));
2783 Textfield: UITextfield,
2784 Textarea: UITextarea,
2785 Checkbox: UICheckbox,
2787 Dropdown: UIDropdown,
2788 DynamicList: UIDynamicList,
2789 Combobox: UICombobox,
2790 ComboButton: UIComboButton,
2791 Hiddenfield: UIHiddenfield,
2792 FileUpload: UIFileUpload