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': L
.dom
.isEmpty(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
|| L
.dom
.isEmpty(panes
[selected
])) {
1649 for (var i
= 0; i
< panes
.length
; i
++) {
1650 if (!L
.dom
.isEmpty(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
);
1665 getPathForPane: function(pane
) {
1666 var path
= [], node
= null;
1668 for (node
= pane
? pane
.parentNode
: null;
1669 node
!= null && node
.hasAttribute
!= null;
1670 node
= node
.parentNode
)
1672 if (node
.hasAttribute('data-tab'))
1673 path
.unshift(node
.getAttribute('data-tab'));
1674 else if (node
.hasAttribute('data-section-id'))
1675 path
.unshift(node
.getAttribute('data-section-id'));
1678 return path
.join('/');
1681 getActiveTabState: function() {
1682 var page
= document
.body
.getAttribute('data-page');
1685 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
1686 if (val
.page
=== page
&& L
.isObject(val
.paths
))
1691 window
.sessionStorage
.removeItem('tab');
1692 return { page
: page
, paths
: {} };
1695 getActiveTabId: function(pane
) {
1696 var path
= this.getPathForPane(pane
);
1697 return +this.getActiveTabState().paths
[path
] || 0;
1700 setActiveTabId: function(pane
, tabIndex
) {
1701 var path
= this.getPathForPane(pane
);
1704 var state
= this.getActiveTabState();
1705 state
.paths
[path
] = tabIndex
;
1707 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
1709 catch (e
) { return false; }
1714 updateTabs: function(ev
, root
) {
1715 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(function(pane
) {
1716 var menu
= pane
.parentNode
.previousElementSibling
,
1717 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
1718 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
1723 if (L
.dom
.isEmpty(pane
)) {
1724 tab
.style
.display
= 'none';
1725 tab
.classList
.remove('flash');
1727 else if (tab
.style
.display
=== 'none') {
1728 tab
.style
.display
= '';
1729 requestAnimationFrame(function() { tab
.classList
.add('flash') });
1733 tab
.setAttribute('data-errors', n_errors
);
1734 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
1735 tab
.setAttribute('data-tooltip-style', 'error');
1738 tab
.removeAttribute('data-errors');
1739 tab
.removeAttribute('data-tooltip');
1744 switchTab: function(ev
) {
1745 var tab
= ev
.target
.parentNode
,
1746 name
= tab
.getAttribute('data-tab'),
1747 menu
= tab
.parentNode
,
1748 group
= menu
.nextElementSibling
,
1749 groupId
= +group
.getAttribute('data-tab-group'),
1752 ev
.preventDefault();
1754 if (!tab
.classList
.contains('cbi-tab-disabled'))
1757 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
1758 tab
.classList
.remove('cbi-tab');
1759 tab
.classList
.remove('cbi-tab-disabled');
1761 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
1764 group
.childNodes
.forEach(function(pane
) {
1765 if (L
.dom
.matches(pane
, '[data-tab]')) {
1766 if (pane
.getAttribute('data-tab') === name
) {
1767 pane
.setAttribute('data-tab-active', 'true');
1768 L
.ui
.tabs
.setActiveTabId(pane
, index
);
1771 pane
.setAttribute('data-tab-active', 'false');
1781 changes
: L
.Class
.singleton({
1783 if (!L
.env
.sessionid
)
1786 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
1789 setIndicator: function(n
) {
1790 var i
= document
.querySelector('.uci_change_indicator');
1792 var poll
= document
.getElementById('xhr_poll_status');
1793 i
= poll
.parentNode
.insertBefore(E('a', {
1795 'class': 'uci_change_indicator label notice',
1796 'click': L
.bind(this.displayChanges
, this)
1801 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
1802 i
.classList
.add('flash');
1803 i
.style
.display
= '';
1806 i
.classList
.remove('flash');
1807 i
.style
.display
= 'none';
1811 renderChangeIndicator: function(changes
) {
1814 for (var config
in changes
)
1815 if (changes
.hasOwnProperty(config
))
1816 n_changes
+= changes
[config
].length
;
1818 this.changes
= changes
;
1819 this.setIndicator(n_changes
);
1823 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1824 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1825 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1826 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1827 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1828 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1829 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1830 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1831 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1832 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1835 displayChanges: function() {
1836 var list
= E('div', { 'class': 'uci-change-list' }),
1837 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
1838 E('div', { 'class': 'cbi-section' }, [
1839 E('strong', _('Legend:')),
1840 E('div', { 'class': 'uci-change-legend' }, [
1841 E('div', { 'class': 'uci-change-legend-label' }, [
1842 E('ins', ' '), ' ', _('Section added') ]),
1843 E('div', { 'class': 'uci-change-legend-label' }, [
1844 E('del', ' '), ' ', _('Section removed') ]),
1845 E('div', { 'class': 'uci-change-legend-label' }, [
1846 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1847 E('div', { 'class': 'uci-change-legend-label' }, [
1848 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1850 E('div', { 'class': 'right' }, [
1854 'click': L
.ui
.hideModal
,
1855 'value': _('Dismiss')
1859 'class': 'cbi-button cbi-button-positive important',
1860 'click': L
.bind(this.apply
, this, true),
1861 'value': _('Save & Apply')
1865 'class': 'cbi-button cbi-button-reset',
1866 'click': L
.bind(this.revert
, this),
1867 'value': _('Revert')
1871 for (var config
in this.changes
) {
1872 if (!this.changes
.hasOwnProperty(config
))
1875 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
1877 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
1878 var chg
= this.changes
[config
][i
],
1879 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
1881 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
1887 if (added
!= null && chg
[1] == added
[0])
1888 return '@' + added
[1] + '[-1]';
1893 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
1900 if (chg[0] == 'add')
1901 added = [ chg[1], chg[2] ];
1905 list.appendChild(E('br'));
1906 dlg.classList.add('uci-dialog');
1909 displayStatus: function(type, content) {
1911 var message = L.ui.showModal('', '');
1913 message.classList.add('alert-message');
1914 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1917 L.dom.content(message, content);
1919 if (!this.was_polling) {
1920 this.was_polling = L.Request.poll.active();
1921 L.Request.poll.stop();
1927 if (this.was_polling)
1928 L.Request.poll.start();
1932 rollback: function(checked) {
1934 this.displayStatus('warning spinning',
1935 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
1936 .format(L.env.apply_rollback)));
1938 var call = function(r, data, duration) {
1939 if (r.status === 204) {
1940 L.ui.changes.displayStatus('warning', [
1941 E('h4', _('Configuration has been rolled back!')),
1942 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)),
1943 E('div', { 'class': 'right' }, [
1947 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1948 'value': _('Dismiss')
1952 'class': 'btn cbi-button-action important',
1953 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1954 'value': _('Revert changes')
1958 'class': 'btn cbi-button-negative important',
1959 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1960 'value': _('Apply unchecked')
1968 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1969 window.setTimeout(function() {
1970 L.Request.request(L.url('admin/uci/confirm'), {
1972 timeout: L.env.apply_timeout * 1000,
1973 query: { sid: L.env.sessionid, token: L.env.token }
1978 call({ status: 0 });
1981 this.displayStatus('warning', [
1982 E('h4', _('Device unreachable!')),
1983 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.'))
1988 confirm: function(checked, deadline, override_token) {
1990 var ts = Date.now();
1992 this.displayStatus('notice');
1995 this.confirm_auth = { token: override_token };
1997 var call = function(r, data, duration) {
1998 if (Date.now() >= deadline) {
1999 window.clearTimeout(tt);
2000 L.ui.changes.rollback(checked);
2003 else if (r && (r.status === 200 || r.status === 204)) {
2004 document.dispatchEvent(new CustomEvent('uci-applied'));
2006 L.ui.changes.setIndicator(0);
2007 L.ui.changes.displayStatus('notice',
2008 E('p', _('Configuration has been applied.')));
2010 window.clearTimeout(tt);
2011 window.setTimeout(function() {
2012 //L.ui.changes.displayStatus(false);
2013 window.location = window.location.href.split('#')[0];
2014 }, L.env.apply_display * 1000);
2019 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2020 window.setTimeout(function() {
2021 L.Request.request(L.url('admin/uci/confirm'), {
2023 timeout: L.env.apply_timeout * 1000,
2024 query: L.ui.changes.confirm_auth
2025 }).then(call, call);
2029 var tick = function() {
2030 var now = Date.now();
2032 L.ui.changes.displayStatus('notice spinning',
2033 E('p', _('Waiting for configuration to get applied… %ds')
2034 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2036 if (now >= deadline)
2039 tt = window.setTimeout(tick, 1000 - (now - ts));
2045 /* wait a few seconds for the settings to become effective */
2046 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2049 apply: function(checked) {
2050 this.displayStatus('notice spinning',
2051 E('p', _('Starting configuration apply…')));
2053 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2055 query: { sid: L.env.sessionid, token: L.env.token }
2056 }).then(function(r) {
2057 if (r.status === (checked ? 200 : 204)) {
2058 var tok = null; try { tok = r.json(); } catch(e) {}
2059 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2060 L.ui.changes.confirm_auth = tok;
2062 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2064 else if (checked && r.status === 204) {
2065 L.ui.changes.displayStatus('notice',
2066 E('p', _('There are no changes to apply')));
2068 window.setTimeout(function() {
2069 L.ui.changes.displayStatus(false);
2070 }, L.env.apply_display * 1000);
2073 L.ui.changes.displayStatus('warning',
2074 E('p', _('Apply request failed with status <code>%h</code>')
2075 .format(r.responseText || r.statusText || r.status)));
2077 window.setTimeout(function() {
2078 L.ui.changes.displayStatus(false);
2079 }, L.env.apply_display * 1000);
2084 revert: function() {
2085 this.displayStatus('notice spinning',
2086 E('p', _('Reverting configuration…')));
2088 L.Request.request(L.url('admin/uci/revert'), {
2090 query: { sid: L.env.sessionid, token: L.env.token }
2091 }).then(function(r) {
2092 if (r.status === 200) {
2093 document.dispatchEvent(new CustomEvent('uci-reverted'));
2095 L.ui.changes.setIndicator(0);
2096 L.ui.changes.displayStatus('notice',
2097 E('p', _('Changes have been reverted.')));
2099 window.setTimeout(function() {
2100 //L.ui.changes.displayStatus(false);
2101 window.location = window.location.href.split('#')[0];
2102 }, L.env.apply_display * 1000);
2105 L.ui.changes.displayStatus('warning',
2106 E('p', _('Revert request failed with status <code>%h</code>')
2107 .format(r.statusText || r.status)));
2109 window.setTimeout(function() {
2110 L.ui.changes.displayStatus(false);
2111 }, L.env.apply_display * 1000);
2117 addValidator: function(field, type, optional, vfunc /*, ... */) {
2121 var events = this.varargs(arguments, 3);
2122 if (events.length == 0)
2123 events.push('blur', 'keyup');
2126 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2127 validatorFn = cbiValidator.validate.bind(cbiValidator);
2129 for (var i = 0; i < events.length; i++)
2130 field.addEventListener(events[i], validatorFn);
2139 createHandlerFn: function(ctx, fn /*, ... */) {
2140 if (typeof(fn) == 'string')
2143 if (typeof(fn) != 'function')
2146 return Function.prototype.bind.apply(function() {
2147 var t = arguments[arguments.length - 1].target;
2149 t.classList.add('spinning');
2155 Promise.resolve(fn.apply(ctx, arguments)).then(function() {
2156 t.classList.remove('spinning');
2159 }, this.varargs(arguments, 2, ctx));
2163 Textfield: UITextfield,
2164 Textarea: UITextarea,
2165 Checkbox: UICheckbox,
2167 Dropdown: UIDropdown,
2168 DynamicList: UIDynamicList,
2169 Combobox: UICombobox,
2170 Hiddenfield: UIHiddenfield