c1389a8fdf7c8790510b19aaeaf87878acb65d50
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 UIDynamicList
= UIElement
.extend({
1192 __init__: function(values
, choices
, options
) {
1193 if (!Array
.isArray(values
))
1194 values
= (values
!= null && values
!= '') ? [ values
] : [];
1196 if (typeof(choices
) != 'object')
1199 this.values
= values
;
1200 this.choices
= choices
;
1201 this.options
= Object
.assign({}, options
, {
1207 render: function() {
1209 'id': this.options
.id
,
1210 'class': 'cbi-dynlist'
1211 }, E('div', { 'class': 'add-item' }));
1214 var cbox
= new UICombobox(null, this.choices
, this.options
);
1215 dl
.lastElementChild
.appendChild(cbox
.render());
1218 var inputEl
= E('input', {
1219 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1221 'class': 'cbi-input-text',
1222 'placeholder': this.options
.placeholder
1225 dl
.lastElementChild
.appendChild(inputEl
);
1226 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1228 if (this.options
.datatype
)
1229 L
.ui
.addValidator(inputEl
, this.options
.datatype
,
1230 true, null, 'blur', 'keyup');
1233 for (var i
= 0; i
< this.values
.length
; i
++)
1234 this.addItem(dl
, this.values
[i
],
1235 this.choices
? this.choices
[this.values
[i
]] : null);
1237 return this.bind(dl
);
1240 bind: function(dl
) {
1241 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1242 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1243 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1247 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1248 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1250 L
.dom
.bindClassInstance(dl
, this);
1255 addItem: function(dl
, value
, text
, flash
) {
1257 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1258 E('span', {}, text
|| value
),
1261 'name': this.options
.name
,
1262 'value': value
})]);
1264 dl
.querySelectorAll('.item').forEach(function(item
) {
1268 var hidden
= item
.querySelector('input[type="hidden"]');
1270 if (hidden
&& hidden
.parentNode
!== item
)
1273 if (hidden
&& hidden
.value
=== value
)
1278 var ai
= dl
.querySelector('.add-item');
1279 ai
.parentNode
.insertBefore(new_item
, ai
);
1282 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1293 removeItem: function(dl
, item
) {
1294 var value
= item
.querySelector('input[type="hidden"]').value
;
1295 var sb
= dl
.querySelector('.cbi-dropdown');
1297 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1298 if (li
.getAttribute('data-value') === value
) {
1299 if (li
.hasAttribute('dynlistcustom'))
1300 li
.parentNode
.removeChild(li
);
1302 li
.removeAttribute('unselectable');
1306 item
.parentNode
.removeChild(item
);
1308 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1319 handleClick: function(ev
) {
1320 var dl
= ev
.currentTarget
,
1321 item
= findParent(ev
.target
, '.item');
1324 this.removeItem(dl
, item
);
1326 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1327 var input
= ev
.target
.previousElementSibling
;
1328 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1329 this.addItem(dl
, input
.value
, null, true);
1335 handleDropdownChange: function(ev
) {
1336 var dl
= ev
.currentTarget
,
1337 sbIn
= ev
.detail
.instance
,
1338 sbEl
= ev
.detail
.element
,
1339 sbVal
= ev
.detail
.value
;
1344 sbIn
.setValues(sbEl
, null);
1345 sbVal
.element
.setAttribute('unselectable', '');
1347 if (sbVal
.element
.hasAttribute('created')) {
1348 sbVal
.element
.removeAttribute('created');
1349 sbVal
.element
.setAttribute('dynlistcustom', '');
1352 this.addItem(dl
, sbVal
.value
, sbVal
.text
, true);
1355 handleKeydown: function(ev
) {
1356 var dl
= ev
.currentTarget
,
1357 item
= findParent(ev
.target
, '.item');
1360 switch (ev
.keyCode
) {
1361 case 8: /* backspace */
1362 if (item
.previousElementSibling
)
1363 item
.previousElementSibling
.focus();
1365 this.removeItem(dl
, item
);
1368 case 46: /* delete */
1369 if (item
.nextElementSibling
) {
1370 if (item
.nextElementSibling
.classList
.contains('item'))
1371 item
.nextElementSibling
.focus();
1373 item
.nextElementSibling
.firstElementChild
.focus();
1376 this.removeItem(dl
, item
);
1380 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1381 switch (ev
.keyCode
) {
1382 case 13: /* enter */
1383 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1384 this.addItem(dl
, ev
.target
.value
, null, true);
1385 ev
.target
.value
= '';
1390 ev
.preventDefault();
1396 getValue: function() {
1397 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1398 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1401 for (var i
= 0; i
< items
.length
; i
++)
1402 v
.push(items
[i
].value
);
1404 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1405 input
.classList
.contains('cbi-input-invalid') == false &&
1406 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1407 v
.push(input
.value
);
1412 setValue: function(values
) {
1413 if (!Array
.isArray(values
))
1414 values
= (values
!= null && values
!= '') ? [ values
] : [];
1416 var items
= this.node
.querySelectorAll('.item');
1418 for (var i
= 0; i
< items
.length
; i
++)
1419 if (items
[i
].parentNode
=== this.node
)
1420 this.removeItem(this.node
, items
[i
]);
1422 for (var i
= 0; i
< values
.length
; i
++)
1423 this.addItem(this.node
, values
[i
],
1424 this.choices
? this.choices
[values
[i
]] : null);
1428 var UIHiddenfield
= UIElement
.extend({
1429 __init__: function(value
, options
) {
1431 this.options
= Object
.assign({
1436 render: function() {
1437 var hiddenEl
= E('input', {
1438 'id': this.options
.id
,
1443 return this.bind(hiddenEl
);
1446 bind: function(hiddenEl
) {
1447 this.node
= hiddenEl
;
1449 L
.dom
.bindClassInstance(hiddenEl
, this);
1454 getValue: function() {
1455 return this.node
.value
;
1458 setValue: function(value
) {
1459 this.node
.value
= value
;
1463 var UIFileUpload
= UIElement
.extend({
1464 __init__: function(value
, options
) {
1466 this.options
= Object
.assign({
1468 enable_upload
: true,
1469 enable_remove
: true,
1470 root_directory
: '/etc/luci-uploads'
1474 bind: function(browserEl
) {
1475 this.node
= browserEl
;
1477 this.setUpdateEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1478 this.setChangeEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1480 L
.dom
.bindClassInstance(browserEl
, this);
1485 render: function() {
1486 return L
.resolveDefault(this.value
!= null ? fs
.stat(this.value
) : null).then(L
.bind(function(stat
) {
1489 if (L
.isObject(stat
) && stat
.type
!= 'directory')
1492 if (this.stat
!= null)
1493 label
= [ this.iconForType(this.stat
.type
), ' %s (%1000mB)'.format(this.truncatePath(this.stat
.path
), this.stat
.size
) ];
1494 else if (this.value
!= null)
1495 label
= [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value
), _('File not accessible')) ];
1497 label
= [ _('Select file…') ];
1499 return this.bind(E('div', { 'id': this.options
.id
}, [
1502 'click': L
.ui
.createHandlerFn(this, 'handleFileBrowser')
1505 'class': 'cbi-filebrowser'
1509 'name': this.options
.name
,
1516 truncatePath: function(path
) {
1517 if (path
.length
> 50)
1518 path
= path
.substring(0, 25) + '…' + path
.substring(path
.length
- 25);
1523 iconForType: function(type
) {
1527 'src': L
.resource('cbi/link.gif'),
1528 'title': _('Symbolic link'),
1534 'src': L
.resource('cbi/folder.gif'),
1535 'title': _('Directory'),
1541 'src': L
.resource('cbi/file.gif'),
1548 canonicalizePath: function(path
) {
1549 return path
.replace(/\/{2,}/, '/')
1550 .replace(/\/\.(\/|$)/g, '/')
1551 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1552 .replace(/\/$/, '');
1555 splitPath: function(path
) {
1556 var croot
= this.canonicalizePath(this.options
.root_directory
|| '/'),
1557 cpath
= this.canonicalizePath(path
|| '/');
1559 if (cpath
.length
<= croot
.length
)
1562 if (cpath
.charAt(croot
.length
) != '/')
1565 var parts
= cpath
.substring(croot
.length
+ 1).split(/\//);
1567 parts
.unshift(croot
);
1572 handleUpload: function(path
, list
, ev
) {
1573 var form
= ev
.target
.parentNode
,
1574 fileinput
= form
.querySelector('input[type="file"]'),
1575 nameinput
= form
.querySelector('input[type="text"]'),
1576 filename
= (nameinput
.value
!= null ? nameinput
.value
: '').trim();
1578 ev
.preventDefault();
1580 if (filename
== '' || filename
.match(/\//) || fileinput
.files
[0] == null)
1583 var existing
= list
.filter(function(e
) { return e
.name
== filename
})[0];
1585 if (existing
!= null && existing
.type
== 'directory')
1586 return alert(_('A directory with the same name already exists.'));
1587 else if (existing
!= null && !confirm(_('Overwrite existing file "%s" ?').format(filename
)))
1590 var data
= new FormData();
1592 data
.append('sessionid', L
.env
.sessionid
);
1593 data
.append('filename', path
+ '/' + filename
);
1594 data
.append('filedata', fileinput
.files
[0]);
1596 return L
.Request
.post('/cgi-bin/cgi-upload', data
, {
1597 progress
: L
.bind(function(btn
, ev
) {
1598 btn
.firstChild
.data
= '%.2f%%'.format((ev
.loaded
/ ev
.total
) * 100);
1600 }).then(L
.bind(function(path
, ev
, res
) {
1601 var reply
= res
.json();
1603 if (L
.isObject(reply
) && reply
.failure
)
1604 alert(_('Upload request failed: %s').format(reply
.message
));
1606 return this.handleSelect(path
, null, ev
);
1607 }, this, path
, ev
));
1610 handleDelete: function(path
, fileStat
, ev
) {
1611 var parent
= path
.replace(/\/[^\/]+$/, '') || '/',
1612 name
= path
.replace(/^.+\//, ''),
1615 ev
.preventDefault();
1617 if (fileStat
.type
== 'directory')
1618 msg
= _('Do you really want to recursively delete the directory "%s" ?').format(name
);
1620 msg
= _('Do you really want to delete "%s" ?').format(name
);
1623 var button
= this.node
.firstElementChild
,
1624 hidden
= this.node
.lastElementChild
;
1626 if (path
== hidden
.value
) {
1627 L
.dom
.content(button
, _('Select file…'));
1631 return fs
.remove(path
).then(L
.bind(function(parent
, ev
) {
1632 return this.handleSelect(parent
, null, ev
);
1633 }, this, parent
, ev
)).catch(function(err
) {
1634 alert(_('Delete request failed: %s').format(err
.message
));
1639 renderUpload: function(path
, list
) {
1640 if (!this.options
.enable_upload
)
1646 'class': 'btn cbi-button-positive',
1647 'click': function(ev
) {
1648 var uploadForm
= ev
.target
.nextElementSibling
,
1649 fileInput
= uploadForm
.querySelector('input[type="file"]');
1651 ev
.target
.style
.display
= 'none';
1652 uploadForm
.style
.display
= '';
1655 }, _('Upload file…')),
1656 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1659 'style': 'display:none',
1660 'change': function(ev
) {
1661 var nameinput
= ev
.target
.parentNode
.querySelector('input[type="text"]'),
1662 uploadbtn
= ev
.target
.parentNode
.querySelector('button.cbi-button-save');
1664 nameinput
.value
= ev
.target
.value
.replace(/^.+[\/\\]/, '');
1665 uploadbtn
.disabled
= false;
1670 'click': function(ev
) {
1671 ev
.preventDefault();
1672 ev
.target
.previousElementSibling
.click();
1674 }, [ _('Browse…') ]),
1675 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1677 'class': 'btn cbi-button-save',
1678 'click': L
.ui
.createHandlerFn(this, 'handleUpload', path
, list
),
1680 }, [ _('Upload file') ])
1685 renderListing: function(container
, path
, list
) {
1686 var breadcrumb
= E('p'),
1689 list
.sort(function(a
, b
) {
1690 var isDirA
= (a
.type
== 'directory'),
1691 isDirB
= (b
.type
== 'directory');
1693 if (isDirA
!= isDirB
)
1694 return isDirA
< isDirB
;
1696 return a
.name
> b
.name
;
1699 for (var i
= 0; i
< list
.length
; i
++) {
1700 if (!this.options
.show_hidden
&& list
[i
].name
.charAt(0) == '.')
1703 var entrypath
= this.canonicalizePath(path
+ '/' + list
[i
].name
),
1704 selected
= (entrypath
== this.node
.lastElementChild
.value
),
1705 mtime
= new Date(list
[i
].mtime
* 1000);
1707 rows
.appendChild(E('li', [
1708 E('div', { 'class': 'name' }, [
1709 this.iconForType(list
[i
].type
),
1713 'style': selected
? 'font-weight:bold' : null,
1714 'click': L
.ui
.createHandlerFn(this, 'handleSelect',
1715 entrypath
, list
[i
].type
!= 'directory' ? list
[i
] : null)
1716 }, '%h'.format(list
[i
].name
))
1718 E('div', { 'class': 'mtime hide-xs' }, [
1719 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1720 mtime
.getFullYear(),
1721 mtime
.getMonth() + 1,
1728 selected
? E('button', {
1730 'click': L
.ui
.createHandlerFn(this, 'handleReset')
1731 }, [ _('Deselect') ]) : '',
1732 this.options
.enable_remove
? E('button', {
1733 'class': 'btn cbi-button-negative',
1734 'click': L
.ui
.createHandlerFn(this, 'handleDelete', entrypath
, list
[i
])
1735 }, [ _('Delete') ]) : ''
1740 if (!rows
.firstElementChild
)
1741 rows
.appendChild(E('em', _('No entries in this directory')));
1743 var dirs
= this.splitPath(path
),
1746 for (var i
= 0; i
< dirs
.length
; i
++) {
1747 cur
= cur
? cur
+ '/' + dirs
[i
] : dirs
[i
];
1748 L
.dom
.append(breadcrumb
, [
1752 'click': L
.ui
.createHandlerFn(this, 'handleSelect', cur
|| '/', null)
1753 }, dirs
[i
] != '' ? '%h'.format(dirs
[i
]) : E('em', '(root)')),
1757 L
.dom
.content(container
, [
1760 E('div', { 'class': 'right' }, [
1761 this.renderUpload(path
, list
),
1765 'click': L
.ui
.createHandlerFn(this, 'handleCancel')
1771 handleCancel: function(ev
) {
1772 var button
= this.node
.firstElementChild
,
1773 browser
= button
.nextElementSibling
;
1775 browser
.classList
.remove('open');
1776 button
.style
.display
= '';
1778 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1781 handleReset: function(ev
) {
1782 var button
= this.node
.firstElementChild
,
1783 hidden
= this.node
.lastElementChild
;
1786 L
.dom
.content(button
, _('Select file…'));
1788 this.handleCancel(ev
);
1791 handleSelect: function(path
, fileStat
, ev
) {
1792 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1793 ul
= browser
.querySelector('ul');
1795 if (fileStat
== null) {
1796 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1797 L
.resolveDefault(fs
.list(path
), []).then(L
.bind(this.renderListing
, this, browser
, path
));
1800 var button
= this.node
.firstElementChild
,
1801 hidden
= this.node
.lastElementChild
;
1803 path
= this.canonicalizePath(path
);
1805 L
.dom
.content(button
, [
1806 this.iconForType(fileStat
.type
),
1807 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1810 browser
.classList
.remove('open');
1811 button
.style
.display
= '';
1812 hidden
.value
= path
;
1814 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1815 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1819 handleFileBrowser: function(ev
) {
1820 var button
= ev
.target
,
1821 browser
= button
.nextElementSibling
,
1822 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : this.options
.root_directory
;
1824 if (this.options
.root_directory
.indexOf(path
) != 0)
1825 path
= this.options
.root_directory
;
1827 ev
.preventDefault();
1829 return L
.resolveDefault(fs
.list(path
), []).then(L
.bind(function(button
, browser
, path
, list
) {
1830 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1831 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1834 button
.style
.display
= 'none';
1835 browser
.classList
.add('open');
1837 return this.renderListing(browser
, path
, list
);
1838 }, this, button
, browser
, path
));
1841 getValue: function() {
1842 return this.node
.lastElementChild
.value
;
1845 setValue: function(value
) {
1846 this.node
.lastElementChild
.value
= value
;
1851 return L
.Class
.extend({
1852 __init__: function() {
1853 modalDiv
= document
.body
.appendChild(
1854 L
.dom
.create('div', { id
: 'modal_overlay' },
1855 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1857 tooltipDiv
= document
.body
.appendChild(
1858 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1860 /* setup old aliases */
1861 L
.showModal
= this.showModal
;
1862 L
.hideModal
= this.hideModal
;
1863 L
.showTooltip
= this.showTooltip
;
1864 L
.hideTooltip
= this.hideTooltip
;
1865 L
.itemlist
= this.itemlist
;
1867 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1868 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1869 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1870 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1872 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1873 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1874 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1878 showModal: function(title
, children
/* , ... */) {
1879 var dlg
= modalDiv
.firstElementChild
;
1881 dlg
.setAttribute('class', 'modal');
1883 for (var i
= 2; i
< arguments
.length
; i
++)
1884 dlg
.classList
.add(arguments
[i
]);
1886 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1887 L
.dom
.append(dlg
, children
);
1889 document
.body
.classList
.add('modal-overlay-active');
1894 hideModal: function() {
1895 document
.body
.classList
.remove('modal-overlay-active');
1899 showTooltip: function(ev
) {
1900 var target
= findParent(ev
.target
, '[data-tooltip]');
1905 if (tooltipTimeout
!== null) {
1906 window
.clearTimeout(tooltipTimeout
);
1907 tooltipTimeout
= null;
1910 var rect
= target
.getBoundingClientRect(),
1911 x
= rect
.left
+ window
.pageXOffset
,
1912 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
1914 tooltipDiv
.className
= 'cbi-tooltip';
1915 tooltipDiv
.innerHTML
= '▲ ';
1916 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
1918 if (target
.hasAttribute('data-tooltip-style'))
1919 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
1921 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
1922 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
1923 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
1926 tooltipDiv
.style
.top
= y
+ 'px';
1927 tooltipDiv
.style
.left
= x
+ 'px';
1928 tooltipDiv
.style
.opacity
= 1;
1930 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
1932 detail
: { target
: target
}
1936 hideTooltip: function(ev
) {
1937 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
1938 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
1941 if (tooltipTimeout
!== null) {
1942 window
.clearTimeout(tooltipTimeout
);
1943 tooltipTimeout
= null;
1946 tooltipDiv
.style
.opacity
= 0;
1947 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
1949 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
1952 addNotification: function(title
, children
/*, ... */) {
1953 var mc
= document
.querySelector('#maincontent') || document
.body
;
1954 var msg
= E('div', {
1955 'class': 'alert-message fade-in',
1956 'style': 'display:flex',
1957 'transitionend': function(ev
) {
1958 var node
= ev
.currentTarget
;
1959 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
1960 node
.parentNode
.removeChild(node
);
1963 E('div', { 'style': 'flex:10' }),
1964 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
1967 'style': 'margin-left:auto; margin-top:auto',
1968 'click': function(ev
) {
1969 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
1972 }, [ _('Dismiss') ])
1977 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
1979 L
.dom
.append(msg
.firstElementChild
, children
);
1981 for (var i
= 2; i
< arguments
.length
; i
++)
1982 msg
.classList
.add(arguments
[i
]);
1984 mc
.insertBefore(msg
, mc
.firstElementChild
);
1990 itemlist: function(node
, items
, separators
) {
1993 if (!Array
.isArray(separators
))
1994 separators
= [ separators
|| E('br') ];
1996 for (var i
= 0; i
< items
.length
; i
+= 2) {
1997 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
1998 var sep
= separators
[(i
/2) % separators
.length
],
2001 children
.push(E('span', { class: 'nowrap' }, [
2002 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2006 if ((i
+2) < items
.length
)
2007 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2011 L
.dom
.content(node
, children
);
2017 tabs
: L
.Class
.singleton({
2019 var groups
= [], prevGroup
= null, currGroup
= null;
2021 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2022 var parent
= tab
.parentNode
;
2024 if (L
.dom
.matches(tab
, 'li') && L
.dom
.matches(parent
, 'ul.cbi-tabmenu'))
2027 if (!parent
.hasAttribute('data-tab-group'))
2028 parent
.setAttribute('data-tab-group', groups
.length
);
2030 currGroup
= +parent
.getAttribute('data-tab-group');
2032 if (currGroup
!== prevGroup
) {
2033 prevGroup
= currGroup
;
2035 if (!groups
[currGroup
])
2036 groups
[currGroup
] = [];
2039 groups
[currGroup
].push(tab
);
2042 for (var i
= 0; i
< groups
.length
; i
++)
2043 this.initTabGroup(groups
[i
]);
2045 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2050 initTabGroup: function(panes
) {
2051 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2054 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2055 group
= panes
[0].parentNode
,
2056 groupId
= +group
.getAttribute('data-tab-group'),
2059 if (group
.getAttribute('data-initialized') === 'true')
2062 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2063 var name
= pane
.getAttribute('data-tab'),
2064 title
= pane
.getAttribute('data-tab-title'),
2065 active
= pane
.getAttribute('data-tab-active') === 'true';
2067 menu
.appendChild(E('li', {
2068 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2069 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2073 'click': this.switchTab
.bind(this)
2080 group
.parentNode
.insertBefore(menu
, group
);
2081 group
.setAttribute('data-initialized', true);
2083 if (selected
=== null) {
2084 selected
= this.getActiveTabId(panes
[0]);
2086 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2087 for (var i
= 0; i
< panes
.length
; i
++) {
2088 if (!this.isEmptyPane(panes
[i
])) {
2095 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2096 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2097 panes
[selected
].setAttribute('data-tab-active', 'true');
2099 this.setActiveTabId(panes
[selected
], selected
);
2102 this.updateTabs(group
);
2105 isEmptyPane: function(pane
) {
2106 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2109 getPathForPane: function(pane
) {
2110 var path
= [], node
= null;
2112 for (node
= pane
? pane
.parentNode
: null;
2113 node
!= null && node
.hasAttribute
!= null;
2114 node
= node
.parentNode
)
2116 if (node
.hasAttribute('data-tab'))
2117 path
.unshift(node
.getAttribute('data-tab'));
2118 else if (node
.hasAttribute('data-section-id'))
2119 path
.unshift(node
.getAttribute('data-section-id'));
2122 return path
.join('/');
2125 getActiveTabState: function() {
2126 var page
= document
.body
.getAttribute('data-page');
2129 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2130 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2135 window
.sessionStorage
.removeItem('tab');
2136 return { page
: page
, paths
: {} };
2139 getActiveTabId: function(pane
) {
2140 var path
= this.getPathForPane(pane
);
2141 return +this.getActiveTabState().paths
[path
] || 0;
2144 setActiveTabId: function(pane
, tabIndex
) {
2145 var path
= this.getPathForPane(pane
);
2148 var state
= this.getActiveTabState();
2149 state
.paths
[path
] = tabIndex
;
2151 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2153 catch (e
) { return false; }
2158 updateTabs: function(ev
, root
) {
2159 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2160 var menu
= pane
.parentNode
.previousElementSibling
,
2161 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2162 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2167 if (this.isEmptyPane(pane
)) {
2168 tab
.style
.display
= 'none';
2169 tab
.classList
.remove('flash');
2171 else if (tab
.style
.display
=== 'none') {
2172 tab
.style
.display
= '';
2173 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2177 tab
.setAttribute('data-errors', n_errors
);
2178 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2179 tab
.setAttribute('data-tooltip-style', 'error');
2182 tab
.removeAttribute('data-errors');
2183 tab
.removeAttribute('data-tooltip');
2188 switchTab: function(ev
) {
2189 var tab
= ev
.target
.parentNode
,
2190 name
= tab
.getAttribute('data-tab'),
2191 menu
= tab
.parentNode
,
2192 group
= menu
.nextElementSibling
,
2193 groupId
= +group
.getAttribute('data-tab-group'),
2196 ev
.preventDefault();
2198 if (!tab
.classList
.contains('cbi-tab-disabled'))
2201 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2202 tab
.classList
.remove('cbi-tab');
2203 tab
.classList
.remove('cbi-tab-disabled');
2205 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2208 group
.childNodes
.forEach(function(pane
) {
2209 if (L
.dom
.matches(pane
, '[data-tab]')) {
2210 if (pane
.getAttribute('data-tab') === name
) {
2211 pane
.setAttribute('data-tab-active', 'true');
2212 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2215 pane
.setAttribute('data-tab-active', 'false');
2224 /* Reconnect handling */
2225 pingDevice: function(proto
, ipaddr
) {
2226 var target
= '%s://%s%s?%s'.format(proto
|| 'http', ipaddr
|| window
.location
.host
, L
.resource('icons/loading.gif'), Math
.random());
2228 return new Promise(function(resolveFn
, rejectFn
) {
2229 var img
= new Image();
2231 img
.onload
= resolveFn
;
2232 img
.onerror
= rejectFn
;
2234 window
.setTimeout(rejectFn
, 1000);
2240 awaitReconnect: function(/* ... */) {
2241 var ipaddrs
= arguments
.length
? arguments
: [ window
.location
.host
];
2243 window
.setTimeout(L
.bind(function() {
2244 L
.Poll
.add(L
.bind(function() {
2245 var tasks
= [], reachable
= false;
2247 for (var i
= 0; i
< 2; i
++)
2248 for (var j
= 0; j
< ipaddrs
.length
; j
++)
2249 tasks
.push(this.pingDevice(i
? 'https' : 'http', ipaddrs
[j
])
2250 .then(function(ev
) { reachable
= ev
.target
.src
.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
2252 return Promise
.all(tasks
).then(function() {
2255 window
.location
= reachable
;
2263 changes
: L
.Class
.singleton({
2265 if (!L
.env
.sessionid
)
2268 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2271 setIndicator: function(n
) {
2272 var i
= document
.querySelector('.uci_change_indicator');
2274 var poll
= document
.getElementById('xhr_poll_status');
2275 i
= poll
.parentNode
.insertBefore(E('a', {
2277 'class': 'uci_change_indicator label notice',
2278 'click': L
.bind(this.displayChanges
, this)
2283 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2284 i
.classList
.add('flash');
2285 i
.style
.display
= '';
2288 i
.classList
.remove('flash');
2289 i
.style
.display
= 'none';
2293 renderChangeIndicator: function(changes
) {
2296 for (var config
in changes
)
2297 if (changes
.hasOwnProperty(config
))
2298 n_changes
+= changes
[config
].length
;
2300 this.changes
= changes
;
2301 this.setIndicator(n_changes
);
2305 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2306 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2307 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2308 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2309 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2310 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2311 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2312 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2313 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2314 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2317 displayChanges: function() {
2318 var list
= E('div', { 'class': 'uci-change-list' }),
2319 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2320 E('div', { 'class': 'cbi-section' }, [
2321 E('strong', _('Legend:')),
2322 E('div', { 'class': 'uci-change-legend' }, [
2323 E('div', { 'class': 'uci-change-legend-label' }, [
2324 E('ins', ' '), ' ', _('Section added') ]),
2325 E('div', { 'class': 'uci-change-legend-label' }, [
2326 E('del', ' '), ' ', _('Section removed') ]),
2327 E('div', { 'class': 'uci-change-legend-label' }, [
2328 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2329 E('div', { 'class': 'uci-change-legend-label' }, [
2330 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2332 E('div', { 'class': 'right' }, [
2335 'click': L
.ui
.hideModal
2336 }, [ _('Dismiss') ]), ' ',
2338 'class': 'cbi-button cbi-button-positive important',
2339 'click': L
.bind(this.apply
, this, true)
2340 }, [ _('Save & Apply') ]), ' ',
2342 'class': 'cbi-button cbi-button-reset',
2343 'click': L
.bind(this.revert
, this)
2344 }, [ _('Revert') ])])])
2347 for (var config
in this.changes
) {
2348 if (!this.changes
.hasOwnProperty(config
))
2351 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2353 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2354 var chg
= this.changes
[config
][i
],
2355 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2357 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2363 if (added
!= null && chg
[1] == added
[0])
2364 return '@' + added
[1] + '[-1]';
2369 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2376 if (chg[0] == 'add')
2377 added = [ chg[1], chg[2] ];
2381 list.appendChild(E('br'));
2382 dlg.classList.add('uci-dialog');
2385 displayStatus: function(type, content) {
2387 var message = L.ui.showModal('', '');
2389 message.classList.add('alert-message');
2390 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2393 L.dom.content(message, content);
2395 if (!this.was_polling) {
2396 this.was_polling = L.Request.poll.active();
2397 L.Request.poll.stop();
2403 if (this.was_polling)
2404 L.Request.poll.start();
2408 rollback: function(checked) {
2410 this.displayStatus('warning spinning',
2411 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2412 .format(L.env.apply_rollback)));
2414 var call = function(r, data, duration) {
2415 if (r.status === 204) {
2416 L.ui.changes.displayStatus('warning', [
2417 E('h4', _('Configuration has been rolled back!')),
2418 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)),
2419 E('div', { 'class': 'right' }, [
2422 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2423 }, [ _('Dismiss') ]), ' ',
2425 'class': 'btn cbi-button-action important',
2426 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2427 }, [ _('Revert changes') ]), ' ',
2429 'class': 'btn cbi-button-negative important',
2430 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2431 }, [ _('Apply unchecked') ])
2438 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2439 window.setTimeout(function() {
2440 L.Request.request(L.url('admin/uci/confirm'), {
2442 timeout: L.env.apply_timeout * 1000,
2443 query: { sid: L.env.sessionid, token: L.env.token }
2448 call({ status: 0 });
2451 this.displayStatus('warning', [
2452 E('h4', _('Device unreachable!')),
2453 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.'))
2458 confirm: function(checked, deadline, override_token) {
2460 var ts = Date.now();
2462 this.displayStatus('notice');
2465 this.confirm_auth = { token: override_token };
2467 var call = function(r, data, duration) {
2468 if (Date.now() >= deadline) {
2469 window.clearTimeout(tt);
2470 L.ui.changes.rollback(checked);
2473 else if (r && (r.status === 200 || r.status === 204)) {
2474 document.dispatchEvent(new CustomEvent('uci-applied'));
2476 L.ui.changes.setIndicator(0);
2477 L.ui.changes.displayStatus('notice',
2478 E('p', _('Configuration has been applied.')));
2480 window.clearTimeout(tt);
2481 window.setTimeout(function() {
2482 //L.ui.changes.displayStatus(false);
2483 window.location = window.location.href.split('#')[0];
2484 }, L.env.apply_display * 1000);
2489 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2490 window.setTimeout(function() {
2491 L.Request.request(L.url('admin/uci/confirm'), {
2493 timeout: L.env.apply_timeout * 1000,
2494 query: L.ui.changes.confirm_auth
2495 }).then(call, call);
2499 var tick = function() {
2500 var now = Date.now();
2502 L.ui.changes.displayStatus('notice spinning',
2503 E('p', _('Waiting for configuration to get applied… %ds')
2504 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2506 if (now >= deadline)
2509 tt = window.setTimeout(tick, 1000 - (now - ts));
2515 /* wait a few seconds for the settings to become effective */
2516 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2519 apply: function(checked) {
2520 this.displayStatus('notice spinning',
2521 E('p', _('Starting configuration apply…')));
2523 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2525 query: { sid: L.env.sessionid, token: L.env.token }
2526 }).then(function(r) {
2527 if (r.status === (checked ? 200 : 204)) {
2528 var tok = null; try { tok = r.json(); } catch(e) {}
2529 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2530 L.ui.changes.confirm_auth = tok;
2532 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2534 else if (checked && r.status === 204) {
2535 L.ui.changes.displayStatus('notice',
2536 E('p', _('There are no changes to apply')));
2538 window.setTimeout(function() {
2539 L.ui.changes.displayStatus(false);
2540 }, L.env.apply_display * 1000);
2543 L.ui.changes.displayStatus('warning',
2544 E('p', _('Apply request failed with status <code>%h</code>')
2545 .format(r.responseText || r.statusText || r.status)));
2547 window.setTimeout(function() {
2548 L.ui.changes.displayStatus(false);
2549 }, L.env.apply_display * 1000);
2554 revert: function() {
2555 this.displayStatus('notice spinning',
2556 E('p', _('Reverting configuration…')));
2558 L.Request.request(L.url('admin/uci/revert'), {
2560 query: { sid: L.env.sessionid, token: L.env.token }
2561 }).then(function(r) {
2562 if (r.status === 200) {
2563 document.dispatchEvent(new CustomEvent('uci-reverted'));
2565 L.ui.changes.setIndicator(0);
2566 L.ui.changes.displayStatus('notice',
2567 E('p', _('Changes have been reverted.')));
2569 window.setTimeout(function() {
2570 //L.ui.changes.displayStatus(false);
2571 window.location = window.location.href.split('#')[0];
2572 }, L.env.apply_display * 1000);
2575 L.ui.changes.displayStatus('warning',
2576 E('p', _('Revert request failed with status <code>%h</code>')
2577 .format(r.statusText || r.status)));
2579 window.setTimeout(function() {
2580 L.ui.changes.displayStatus(false);
2581 }, L.env.apply_display * 1000);
2587 addValidator: function(field, type, optional, vfunc /*, ... */) {
2591 var events = this.varargs(arguments, 3);
2592 if (events.length == 0)
2593 events.push('blur', 'keyup');
2596 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2597 validatorFn = cbiValidator.validate.bind(cbiValidator);
2599 for (var i = 0; i < events.length; i++)
2600 field.addEventListener(events[i], validatorFn);
2609 createHandlerFn: function(ctx, fn /*, ... */) {
2610 if (typeof(fn) == 'string')
2613 if (typeof(fn) != 'function')
2616 return Function.prototype.bind.apply(function() {
2617 var t = arguments[arguments.length - 1].target;
2619 t.classList.add('spinning');
2625 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2626 t.classList.remove('spinning');
2629 }, this.varargs(arguments, 2, ctx));
2633 Textfield: UITextfield,
2634 Textarea: UITextarea,
2635 Checkbox: UICheckbox,
2637 Dropdown: UIDropdown,
2638 DynamicList: UIDynamicList,
2639 Combobox: UICombobox,
2640 Hiddenfield: UIHiddenfield,
2641 FileUpload: UIFileUpload