10 var UIElement
= L
.Class
.extend({
11 getValue: function() {
12 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
13 return this.node
.value
;
18 setValue: function(value
) {
19 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
20 this.node
.value
= value
;
24 return (this.validState
!== false);
27 triggerValidation: function() {
28 if (typeof(this.vfunc
) != 'function')
31 var wasValid
= this.isValid();
35 return (wasValid
!= this.isValid());
38 registerEvents: function(targetNode
, synevent
, events
) {
39 var dispatchFn
= L
.bind(function(ev
) {
40 this.node
.dispatchEvent(new CustomEvent(synevent
, { bubbles
: true }));
43 for (var i
= 0; i
< events
.length
; i
++)
44 targetNode
.addEventListener(events
[i
], dispatchFn
);
47 setUpdateEvents: function(targetNode
/*, ... */) {
48 var datatype
= this.options
.datatype
,
49 optional
= this.options
.hasOwnProperty('optional') ? this.options
.optional
: true,
50 validate
= this.options
.validate
,
51 events
= this.varargs(arguments
, 1);
53 this.registerEvents(targetNode
, 'widget-update', events
);
55 if (!datatype
&& !validate
)
58 this.vfunc
= L
.ui
.addValidator
.apply(L
.ui
, [
59 targetNode
, datatype
|| 'string',
63 this.node
.addEventListener('validation-success', L
.bind(function(ev
) {
64 this.validState
= true;
67 this.node
.addEventListener('validation-failure', L
.bind(function(ev
) {
68 this.validState
= false;
72 setChangeEvents: function(targetNode
/*, ... */) {
73 this.registerEvents(targetNode
, 'widget-change', this.varargs(arguments
, 1));
77 var UITextfield
= UIElement
.extend({
78 __init__: function(value
, options
) {
80 this.options
= Object
.assign({
87 var frameEl
= E('div', { 'id': this.options
.id
});
89 if (this.options
.password
) {
90 frameEl
.classList
.add('nowrap');
91 frameEl
.appendChild(E('input', {
93 'style': 'position:absolute; left:-100000px',
96 'name': this.options
.name
? 'password.%s'.format(this.options
.name
) : null
100 frameEl
.appendChild(E('input', {
101 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
102 'name': this.options
.name
,
103 'type': this.options
.password
? 'password' : 'text',
104 'class': this.options
.password
? 'cbi-input-password' : 'cbi-input-text',
105 'readonly': this.options
.readonly
? '' : null,
106 'maxlength': this.options
.maxlength
,
107 'placeholder': this.options
.placeholder
,
111 if (this.options
.password
)
112 frameEl
.appendChild(E('button', {
113 'class': 'cbi-button cbi-button-neutral',
114 'title': _('Reveal/hide password'),
115 'aria-label': _('Reveal/hide password'),
116 'click': function(ev
) {
117 var e
= this.previousElementSibling
;
118 e
.type
= (e
.type
=== 'password') ? 'text' : 'password';
123 return this.bind(frameEl
);
126 bind: function(frameEl
) {
127 var inputEl
= frameEl
.childNodes
[+!!this.options
.password
];
131 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
132 this.setChangeEvents(inputEl
, 'change');
134 L
.dom
.bindClassInstance(frameEl
, this);
139 getValue: function() {
140 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
141 return inputEl
.value
;
144 setValue: function(value
) {
145 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
146 inputEl
.value
= value
;
150 var UITextarea
= UIElement
.extend({
151 __init__: function(value
, options
) {
153 this.options
= Object
.assign({
162 var frameEl
= E('div', { 'id': this.options
.id
}),
163 value
= (this.value
!= null) ? String(this.value
) : '';
165 frameEl
.appendChild(E('textarea', {
166 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
167 'name': this.options
.name
,
168 'class': 'cbi-input-textarea',
169 'readonly': this.options
.readonly
? '' : null,
170 'placeholder': this.options
.placeholder
,
171 'style': !this.options
.cols
? 'width:100%' : null,
172 'cols': this.options
.cols
,
173 'rows': this.options
.rows
,
174 'wrap': this.options
.wrap
? '' : null
177 if (this.options
.monospace
)
178 frameEl
.firstElementChild
.style
.fontFamily
= 'monospace';
180 return this.bind(frameEl
);
183 bind: function(frameEl
) {
184 var inputEl
= frameEl
.firstElementChild
;
188 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
189 this.setChangeEvents(inputEl
, 'change');
191 L
.dom
.bindClassInstance(frameEl
, this);
196 getValue: function() {
197 return this.node
.firstElementChild
.value
;
200 setValue: function(value
) {
201 this.node
.firstElementChild
.value
= value
;
205 var UICheckbox
= UIElement
.extend({
206 __init__: function(value
, options
) {
208 this.options
= Object
.assign({
215 var frameEl
= E('div', {
216 'id': this.options
.id
,
217 'class': 'cbi-checkbox'
220 if (this.options
.hiddenname
)
221 frameEl
.appendChild(E('input', {
223 'name': this.options
.hiddenname
,
227 frameEl
.appendChild(E('input', {
228 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
229 'name': this.options
.name
,
231 'value': this.options
.value_enabled
,
232 'checked': (this.value
== this.options
.value_enabled
) ? '' : null
235 return this.bind(frameEl
);
238 bind: function(frameEl
) {
241 this.setUpdateEvents(frameEl
.lastElementChild
, 'click', 'blur');
242 this.setChangeEvents(frameEl
.lastElementChild
, 'change');
244 L
.dom
.bindClassInstance(frameEl
, this);
249 isChecked: function() {
250 return this.node
.lastElementChild
.checked
;
253 getValue: function() {
254 return this.isChecked()
255 ? this.options
.value_enabled
256 : this.options
.value_disabled
;
259 setValue: function(value
) {
260 this.node
.lastElementChild
.checked
= (value
== this.options
.value_enabled
);
264 var UISelect
= UIElement
.extend({
265 __init__: function(value
, choices
, options
) {
266 if (!L
.isObject(choices
))
269 if (!Array
.isArray(value
))
270 value
= (value
!= null && value
!= '') ? [ value
] : [];
272 if (!options
.multiple
&& value
.length
> 1)
276 this.choices
= choices
;
277 this.options
= Object
.assign({
280 orientation
: 'horizontal'
283 if (this.choices
.hasOwnProperty(''))
284 this.options
.optional
= true;
288 var frameEl
= E('div', { 'id': this.options
.id
}),
289 keys
= Object
.keys(this.choices
);
291 if (this.options
.sort
=== true)
293 else if (Array
.isArray(this.options
.sort
))
294 keys
= this.options
.sort
;
296 if (this.options
.widget
== 'select') {
297 frameEl
.appendChild(E('select', {
298 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
299 'name': this.options
.name
,
300 'size': this.options
.size
,
301 'class': 'cbi-input-select',
302 'multiple': this.options
.multiple
? '' : null
305 if (this.options
.optional
)
306 frameEl
.lastChild
.appendChild(E('option', {
308 'selected': (this.values
.length
== 0 || this.values
[0] == '') ? '' : null
309 }, this.choices
[''] || this.options
.placeholder
|| _('-- Please choose --')));
311 for (var i
= 0; i
< keys
.length
; i
++) {
312 if (keys
[i
] == null || keys
[i
] == '')
315 frameEl
.lastChild
.appendChild(E('option', {
317 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
318 }, this.choices
[keys
[i
]] || keys
[i
]));
322 var brEl
= (this.options
.orientation
=== 'horizontal') ? document
.createTextNode(' ') : E('br');
324 for (var i
= 0; i
< keys
.length
; i
++) {
325 frameEl
.appendChild(E('label', {}, [
327 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
328 'name': this.options
.id
|| this.options
.name
,
329 'type': this.options
.multiple
? 'checkbox' : 'radio',
330 'class': this.options
.multiple
? 'cbi-input-checkbox' : 'cbi-input-radio',
332 'checked': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
334 this.choices
[keys
[i
]] || keys
[i
]
337 if (i
+ 1 == this.options
.size
)
338 frameEl
.appendChild(brEl
);
342 return this.bind(frameEl
);
345 bind: function(frameEl
) {
348 if (this.options
.widget
== 'select') {
349 this.setUpdateEvents(frameEl
.firstChild
, 'change', 'click', 'blur');
350 this.setChangeEvents(frameEl
.firstChild
, 'change');
353 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
354 for (var i
= 0; i
< radioEls
.length
; i
++) {
355 this.setUpdateEvents(radioEls
[i
], 'change', 'click', 'blur');
356 this.setChangeEvents(radioEls
[i
], 'change', 'click', 'blur');
360 L
.dom
.bindClassInstance(frameEl
, this);
365 getValue: function() {
366 if (this.options
.widget
== 'select')
367 return this.node
.firstChild
.value
;
369 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
370 for (var i
= 0; i
< radioEls
.length
; i
++)
371 if (radioEls
[i
].checked
)
372 return radioEls
[i
].value
;
377 setValue: function(value
) {
378 if (this.options
.widget
== 'select') {
382 for (var i
= 0; i
< this.node
.firstChild
.options
.length
; i
++)
383 this.node
.firstChild
.options
[i
].selected
= (this.node
.firstChild
.options
[i
].value
== value
);
388 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
389 for (var i
= 0; i
< radioEls
.length
; i
++)
390 radioEls
[i
].checked
= (radioEls
[i
].value
== value
);
394 var UIDropdown
= UIElement
.extend({
395 __init__: function(value
, choices
, options
) {
396 if (typeof(choices
) != 'object')
399 if (!Array
.isArray(value
))
400 this.values
= (value
!= null && value
!= '') ? [ value
] : [];
404 this.choices
= choices
;
405 this.options
= Object
.assign({
407 multiple
: Array
.isArray(value
),
409 select_placeholder
: _('-- Please choose --'),
410 custom_placeholder
: _('-- custom --'),
414 create_query
: '.create-item-input',
415 create_template
: 'script[type="item-template"]'
421 'id': this.options
.id
,
422 'class': 'cbi-dropdown',
423 'multiple': this.options
.multiple
? '' : null,
424 'optional': this.options
.optional
? '' : null,
427 var keys
= Object
.keys(this.choices
);
429 if (this.options
.sort
=== true)
431 else if (Array
.isArray(this.options
.sort
))
432 keys
= this.options
.sort
;
434 if (this.options
.create
)
435 for (var i
= 0; i
< this.values
.length
; i
++)
436 if (!this.choices
.hasOwnProperty(this.values
[i
]))
437 keys
.push(this.values
[i
]);
439 for (var i
= 0; i
< keys
.length
; i
++)
440 sb
.lastElementChild
.appendChild(E('li', {
441 'data-value': keys
[i
],
442 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
443 }, this.choices
[keys
[i
]] || keys
[i
]));
445 if (this.options
.create
) {
446 var createEl
= E('input', {
448 'class': 'create-item-input',
449 'readonly': this.options
.readonly
? '' : null,
450 'maxlength': this.options
.maxlength
,
451 'placeholder': this.options
.custom_placeholder
|| this.options
.placeholder
454 if (this.options
.datatype
)
455 L
.ui
.addValidator(createEl
, this.options
.datatype
,
456 true, null, 'blur', 'keyup');
458 sb
.lastElementChild
.appendChild(E('li', { 'data-value': '-' }, createEl
));
461 if (this.options
.create_markup
)
462 sb
.appendChild(E('script', { type
: 'item-template' },
463 this.options
.create_markup
));
465 return this.bind(sb
);
469 var o
= this.options
;
471 o
.multiple
= sb
.hasAttribute('multiple');
472 o
.optional
= sb
.hasAttribute('optional');
473 o
.placeholder
= sb
.getAttribute('placeholder') || o
.placeholder
;
474 o
.display_items
= parseInt(sb
.getAttribute('display-items') || o
.display_items
);
475 o
.dropdown_items
= parseInt(sb
.getAttribute('dropdown-items') || o
.dropdown_items
);
476 o
.create_query
= sb
.getAttribute('item-create') || o
.create_query
;
477 o
.create_template
= sb
.getAttribute('item-template') || o
.create_template
;
479 var ul
= sb
.querySelector('ul'),
480 more
= sb
.appendChild(E('span', { class: 'more', tabindex
: -1 }, '···')),
481 open
= sb
.appendChild(E('span', { class: 'open', tabindex
: -1 }, '▾')),
482 canary
= sb
.appendChild(E('div')),
483 create
= sb
.querySelector(this.options
.create_query
),
484 ndisplay
= this.options
.display_items
,
487 if (this.options
.multiple
) {
488 var items
= ul
.querySelectorAll('li');
490 for (var i
= 0; i
< items
.length
; i
++) {
491 this.transformItem(sb
, items
[i
]);
493 if (items
[i
].hasAttribute('selected') && ndisplay
-- > 0)
494 items
[i
].setAttribute('display', n
++);
498 if (this.options
.optional
&& !ul
.querySelector('li[data-value=""]')) {
499 var placeholder
= E('li', { placeholder
: '' },
500 this.options
.select_placeholder
|| this.options
.placeholder
);
503 ? ul
.insertBefore(placeholder
, ul
.firstChild
)
504 : ul
.appendChild(placeholder
);
507 var items
= ul
.querySelectorAll('li'),
508 sel
= sb
.querySelectorAll('[selected]');
510 sel
.forEach(function(s
) {
511 s
.removeAttribute('selected');
514 var s
= sel
[0] || items
[0];
516 s
.setAttribute('selected', '');
517 s
.setAttribute('display', n
++);
523 this.saveValues(sb
, ul
);
525 ul
.setAttribute('tabindex', -1);
526 sb
.setAttribute('tabindex', 0);
529 sb
.setAttribute('more', '')
531 sb
.removeAttribute('more');
533 if (ndisplay
== this.options
.display_items
)
534 sb
.setAttribute('empty', '')
536 sb
.removeAttribute('empty');
538 L
.dom
.content(more
, (ndisplay
== this.options
.display_items
)
539 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
542 sb
.addEventListener('click', this.handleClick
.bind(this));
543 sb
.addEventListener('keydown', this.handleKeydown
.bind(this));
544 sb
.addEventListener('cbi-dropdown-close', this.handleDropdownClose
.bind(this));
545 sb
.addEventListener('cbi-dropdown-select', this.handleDropdownSelect
.bind(this));
547 if ('ontouchstart' in window
) {
548 sb
.addEventListener('touchstart', function(ev
) { ev
.stopPropagation(); });
549 window
.addEventListener('touchstart', this.closeAllDropdowns
);
552 sb
.addEventListener('mouseover', this.handleMouseover
.bind(this));
553 sb
.addEventListener('focus', this.handleFocus
.bind(this));
555 canary
.addEventListener('focus', this.handleCanaryFocus
.bind(this));
557 window
.addEventListener('mouseover', this.setFocus
);
558 window
.addEventListener('click', this.closeAllDropdowns
);
562 create
.addEventListener('keydown', this.handleCreateKeydown
.bind(this));
563 create
.addEventListener('focus', this.handleCreateFocus
.bind(this));
564 create
.addEventListener('blur', this.handleCreateBlur
.bind(this));
566 var li
= findParent(create
, 'li');
568 li
.setAttribute('unselectable', '');
569 li
.addEventListener('click', this.handleCreateClick
.bind(this));
574 this.setUpdateEvents(sb
, 'cbi-dropdown-open', 'cbi-dropdown-close');
575 this.setChangeEvents(sb
, 'cbi-dropdown-change', 'cbi-dropdown-close');
577 L
.dom
.bindClassInstance(sb
, this);
582 openDropdown: function(sb
) {
583 var st
= window
.getComputedStyle(sb
, null),
584 ul
= sb
.querySelector('ul'),
585 li
= ul
.querySelectorAll('li'),
586 fl
= findParent(sb
, '.cbi-value-field'),
587 sel
= ul
.querySelector('[selected]'),
588 rect
= sb
.getBoundingClientRect(),
589 items
= Math
.min(this.options
.dropdown_items
, li
.length
);
591 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
592 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
595 sb
.setAttribute('open', '');
597 var pv
= ul
.cloneNode(true);
598 pv
.classList
.add('preview');
601 fl
.classList
.add('cbi-dropdown-open');
603 if ('ontouchstart' in window
) {
604 var vpWidth
= Math
.max(document
.documentElement
.clientWidth
, window
.innerWidth
|| 0),
605 vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
606 scrollFrom
= window
.pageYOffset
,
607 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
610 ul
.style
.top
= sb
.offsetHeight
+ 'px';
611 ul
.style
.left
= -rect
.left
+ 'px';
612 ul
.style
.right
= (rect
.right
- vpWidth
) + 'px';
613 ul
.style
.maxHeight
= (vpHeight
* 0.5) + 'px';
614 ul
.style
.WebkitOverflowScrolling
= 'touch';
616 var scrollStep = function(timestamp
) {
619 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
622 var duration
= Math
.max(timestamp
- start
, 1);
623 if (duration
< 100) {
624 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
625 window
.requestAnimationFrame(scrollStep
);
628 document
.body
.scrollTop
= scrollTo
;
632 window
.requestAnimationFrame(scrollStep
);
635 ul
.style
.maxHeight
= '1px';
636 ul
.style
.top
= ul
.style
.bottom
= '';
638 window
.requestAnimationFrame(function() {
639 var itemHeight
= li
[Math
.max(0, li
.length
- 2)].getBoundingClientRect().height
,
641 spaceAbove
= rect
.top
,
642 spaceBelow
= window
.innerHeight
- rect
.height
- rect
.top
;
644 for (var i
= 0; i
< (items
== -1 ? li
.length
: items
); i
++)
645 fullHeight
+= li
[i
].getBoundingClientRect().height
;
647 if (fullHeight
<= spaceBelow
) {
648 ul
.style
.top
= rect
.height
+ 'px';
649 ul
.style
.maxHeight
= spaceBelow
+ 'px';
651 else if (fullHeight
<= spaceAbove
) {
652 ul
.style
.bottom
= rect
.height
+ 'px';
653 ul
.style
.maxHeight
= spaceAbove
+ 'px';
655 else if (spaceBelow
>= spaceAbove
) {
656 ul
.style
.top
= rect
.height
+ 'px';
657 ul
.style
.maxHeight
= (spaceBelow
- (spaceBelow
% itemHeight
)) + 'px';
660 ul
.style
.bottom
= rect
.height
+ 'px';
661 ul
.style
.maxHeight
= (spaceAbove
- (spaceAbove
% itemHeight
)) + 'px';
664 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
668 var cboxes
= ul
.querySelectorAll('[selected] input[type="checkbox"]');
669 for (var i
= 0; i
< cboxes
.length
; i
++) {
670 cboxes
[i
].checked
= true;
671 cboxes
[i
].disabled
= (cboxes
.length
== 1 && !this.options
.optional
);
674 ul
.classList
.add('dropdown');
676 sb
.insertBefore(pv
, ul
.nextElementSibling
);
678 li
.forEach(function(l
) {
679 l
.setAttribute('tabindex', 0);
682 sb
.lastElementChild
.setAttribute('tabindex', 0);
684 this.setFocus(sb
, sel
|| li
[0], true);
687 closeDropdown: function(sb
, no_focus
) {
688 if (!sb
.hasAttribute('open'))
691 var pv
= sb
.querySelector('ul.preview'),
692 ul
= sb
.querySelector('ul.dropdown'),
693 li
= ul
.querySelectorAll('li'),
694 fl
= findParent(sb
, '.cbi-value-field');
696 li
.forEach(function(l
) { l
.removeAttribute('tabindex'); });
697 sb
.lastElementChild
.removeAttribute('tabindex');
700 sb
.removeAttribute('open');
701 sb
.style
.width
= sb
.style
.height
= '';
703 ul
.classList
.remove('dropdown');
704 ul
.style
.top
= ul
.style
.bottom
= ul
.style
.maxHeight
= '';
707 fl
.classList
.remove('cbi-dropdown-open');
710 this.setFocus(sb
, sb
);
712 this.saveValues(sb
, ul
);
715 toggleItem: function(sb
, li
, force_state
) {
716 if (li
.hasAttribute('unselectable'))
719 if (this.options
.multiple
) {
720 var cbox
= li
.querySelector('input[type="checkbox"]'),
721 items
= li
.parentNode
.querySelectorAll('li'),
722 label
= sb
.querySelector('ul.preview'),
723 sel
= li
.parentNode
.querySelectorAll('[selected]').length
,
724 more
= sb
.querySelector('.more'),
725 ndisplay
= this.options
.display_items
,
728 if (li
.hasAttribute('selected')) {
729 if (force_state
!== true) {
730 if (sel
> 1 || this.options
.optional
) {
731 li
.removeAttribute('selected');
732 cbox
.checked
= cbox
.disabled
= false;
736 cbox
.disabled
= true;
741 if (force_state
!== false) {
742 li
.setAttribute('selected', '');
744 cbox
.disabled
= false;
749 while (label
&& label
.firstElementChild
)
750 label
.removeChild(label
.firstElementChild
);
752 for (var i
= 0; i
< items
.length
; i
++) {
753 items
[i
].removeAttribute('display');
754 if (items
[i
].hasAttribute('selected')) {
755 if (ndisplay
-- > 0) {
756 items
[i
].setAttribute('display', n
++);
758 label
.appendChild(items
[i
].cloneNode(true));
760 var c
= items
[i
].querySelector('input[type="checkbox"]');
762 c
.disabled
= (sel
== 1 && !this.options
.optional
);
767 sb
.setAttribute('more', '');
769 sb
.removeAttribute('more');
771 if (ndisplay
=== this.options
.display_items
)
772 sb
.setAttribute('empty', '');
774 sb
.removeAttribute('empty');
776 L
.dom
.content(more
, (ndisplay
=== this.options
.display_items
)
777 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
780 var sel
= li
.parentNode
.querySelector('[selected]');
782 sel
.removeAttribute('display');
783 sel
.removeAttribute('selected');
786 li
.setAttribute('display', 0);
787 li
.setAttribute('selected', '');
789 this.closeDropdown(sb
, true);
792 this.saveValues(sb
, li
.parentNode
);
795 transformItem: function(sb
, li
) {
796 var cbox
= E('form', {}, E('input', { type
: 'checkbox', tabindex
: -1, onclick
: 'event.preventDefault()' })),
799 while (li
.firstChild
)
800 label
.appendChild(li
.firstChild
);
802 li
.appendChild(cbox
);
803 li
.appendChild(label
);
806 saveValues: function(sb
, ul
) {
807 var sel
= ul
.querySelectorAll('li[selected]'),
808 div
= sb
.lastElementChild
,
809 name
= this.options
.name
,
813 while (div
.lastElementChild
)
814 div
.removeChild(div
.lastElementChild
);
816 sel
.forEach(function (s
) {
817 if (s
.hasAttribute('placeholder'))
822 value
: s
.hasAttribute('data-value') ? s
.getAttribute('data-value') : s
.innerText
,
826 div
.appendChild(E('input', {
834 strval
+= strval
.length
? ' ' + v
.value
: v
.value
;
842 if (this.options
.multiple
)
843 detail
.values
= values
;
845 detail
.value
= values
.length
? values
[0] : null;
849 sb
.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
855 setValues: function(sb
, values
) {
856 var ul
= sb
.querySelector('ul');
858 if (this.options
.create
) {
859 for (var value
in values
) {
860 this.createItems(sb
, value
);
862 if (!this.options
.multiple
)
867 if (this.options
.multiple
) {
868 var lis
= ul
.querySelectorAll('li[data-value]');
869 for (var i
= 0; i
< lis
.length
; i
++) {
870 var value
= lis
[i
].getAttribute('data-value');
871 if (values
=== null || !(value
in values
))
872 this.toggleItem(sb
, lis
[i
], false);
874 this.toggleItem(sb
, lis
[i
], true);
878 var ph
= ul
.querySelector('li[placeholder]');
880 this.toggleItem(sb
, ph
);
882 var lis
= ul
.querySelectorAll('li[data-value]');
883 for (var i
= 0; i
< lis
.length
; i
++) {
884 var value
= lis
[i
].getAttribute('data-value');
885 if (values
!== null && (value
in values
))
886 this.toggleItem(sb
, lis
[i
]);
891 setFocus: function(sb
, elem
, scroll
) {
892 if (sb
&& sb
.hasAttribute
&& sb
.hasAttribute('locked-in'))
895 if (sb
.target
&& findParent(sb
.target
, 'ul.dropdown'))
898 document
.querySelectorAll('.focus').forEach(function(e
) {
899 if (!matchesElem(e
, 'input')) {
900 e
.classList
.remove('focus');
907 elem
.classList
.add('focus');
910 elem
.parentNode
.scrollTop
= elem
.offsetTop
- elem
.parentNode
.offsetTop
;
914 createItems: function(sb
, value
) {
916 val
= (value
|| '').trim(),
917 ul
= sb
.querySelector('ul');
919 if (!sbox
.options
.multiple
)
920 val
= val
.length
? [ val
] : [];
922 val
= val
.length
? val
.split(/\s+/) : [];
924 val
.forEach(function(item
) {
927 ul
.childNodes
.forEach(function(li
) {
928 if (li
.getAttribute
&& li
.getAttribute('data-value') === item
)
934 tpl
= sb
.querySelector(sbox
.options
.create_template
);
937 markup
= (tpl
.textContent
|| tpl
.innerHTML
|| tpl
.firstChild
.data
).replace(/^<!--|-->$/, '').trim();
939 markup
= '<li data-value="{{value}}">{{value}}</li>';
941 new_item
= E(markup
.replace(/{{value}}/g, '%h'.format(item
)));
943 if (sbox
.options
.multiple
) {
944 sbox
.transformItem(sb
, new_item
);
947 var old
= ul
.querySelector('li[created]');
951 new_item
.setAttribute('created', '');
954 new_item
= ul
.insertBefore(new_item
, ul
.lastElementChild
);
957 sbox
.toggleItem(sb
, new_item
, true);
958 sbox
.setFocus(sb
, new_item
, true);
962 closeAllDropdowns: function() {
963 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
964 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
968 handleClick: function(ev
) {
969 var sb
= ev
.currentTarget
;
971 if (!sb
.hasAttribute('open')) {
972 if (!matchesElem(ev
.target
, 'input'))
973 this.openDropdown(sb
);
976 var li
= findParent(ev
.target
, 'li');
977 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
978 this.toggleItem(sb
, li
);
979 else if (li
&& li
.parentNode
.classList
.contains('preview'))
980 this.closeDropdown(sb
);
981 else if (matchesElem(ev
.target
, 'span.open, span.more'))
982 this.closeDropdown(sb
);
986 ev
.stopPropagation();
989 handleKeydown: function(ev
) {
990 var sb
= ev
.currentTarget
;
992 if (matchesElem(ev
.target
, 'input'))
995 if (!sb
.hasAttribute('open')) {
996 switch (ev
.keyCode
) {
1001 this.openDropdown(sb
);
1002 ev
.preventDefault();
1006 var active
= findParent(document
.activeElement
, 'li');
1008 switch (ev
.keyCode
) {
1010 this.closeDropdown(sb
);
1015 if (!active
.hasAttribute('selected'))
1016 this.toggleItem(sb
, active
);
1017 this.closeDropdown(sb
);
1018 ev
.preventDefault();
1024 this.toggleItem(sb
, active
);
1025 ev
.preventDefault();
1030 if (active
&& active
.previousElementSibling
) {
1031 this.setFocus(sb
, active
.previousElementSibling
);
1032 ev
.preventDefault();
1037 if (active
&& active
.nextElementSibling
) {
1038 this.setFocus(sb
, active
.nextElementSibling
);
1039 ev
.preventDefault();
1046 handleDropdownClose: function(ev
) {
1047 var sb
= ev
.currentTarget
;
1049 this.closeDropdown(sb
, true);
1052 handleDropdownSelect: function(ev
) {
1053 var sb
= ev
.currentTarget
,
1054 li
= findParent(ev
.target
, 'li');
1059 this.toggleItem(sb
, li
);
1060 this.closeDropdown(sb
, true);
1063 handleMouseover: function(ev
) {
1064 var sb
= ev
.currentTarget
;
1066 if (!sb
.hasAttribute('open'))
1069 var li
= findParent(ev
.target
, 'li');
1071 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1072 this.setFocus(sb
, li
);
1075 handleFocus: function(ev
) {
1076 var sb
= ev
.currentTarget
;
1078 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1079 if (s
!== sb
|| sb
.hasAttribute('open'))
1080 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1084 handleCanaryFocus: function(ev
) {
1085 this.closeDropdown(ev
.currentTarget
.parentNode
);
1088 handleCreateKeydown: function(ev
) {
1089 var input
= ev
.currentTarget
,
1090 sb
= findParent(input
, '.cbi-dropdown');
1092 switch (ev
.keyCode
) {
1094 ev
.preventDefault();
1096 if (input
.classList
.contains('cbi-input-invalid'))
1099 this.createItems(sb
, input
.value
);
1106 handleCreateFocus: function(ev
) {
1107 var input
= ev
.currentTarget
,
1108 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1109 sb
= findParent(input
, '.cbi-dropdown');
1112 cbox
.checked
= true;
1114 sb
.setAttribute('locked-in', '');
1117 handleCreateBlur: function(ev
) {
1118 var input
= ev
.currentTarget
,
1119 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1120 sb
= findParent(input
, '.cbi-dropdown');
1123 cbox
.checked
= false;
1125 sb
.removeAttribute('locked-in');
1128 handleCreateClick: function(ev
) {
1129 ev
.currentTarget
.querySelector(this.options
.create_query
).focus();
1132 setValue: function(values
) {
1133 if (this.options
.multiple
) {
1134 if (!Array
.isArray(values
))
1135 values
= (values
!= null && values
!= '') ? [ values
] : [];
1139 for (var i
= 0; i
< values
.length
; i
++)
1140 v
[values
[i
]] = true;
1142 this.setValues(this.node
, v
);
1147 if (values
!= null) {
1148 if (Array
.isArray(values
))
1149 v
[values
[0]] = true;
1154 this.setValues(this.node
, v
);
1158 getValue: function() {
1159 var div
= this.node
.lastElementChild
,
1160 h
= div
.querySelectorAll('input[type="hidden"]'),
1163 for (var i
= 0; i
< h
.length
; i
++)
1166 return this.options
.multiple
? v
: v
[0];
1170 var UICombobox
= UIDropdown
.extend({
1171 __init__: function(value
, choices
, options
) {
1172 this.super('__init__', [ value
, choices
, Object
.assign({
1173 select_placeholder
: _('-- Please choose --'),
1174 custom_placeholder
: _('-- custom --'),
1185 var UIDynamicList
= UIElement
.extend({
1186 __init__: function(values
, choices
, options
) {
1187 if (!Array
.isArray(values
))
1188 values
= (values
!= null && values
!= '') ? [ values
] : [];
1190 if (typeof(choices
) != 'object')
1193 this.values
= values
;
1194 this.choices
= choices
;
1195 this.options
= Object
.assign({}, options
, {
1201 render: function() {
1203 'id': this.options
.id
,
1204 'class': 'cbi-dynlist'
1205 }, E('div', { 'class': 'add-item' }));
1208 var cbox
= new UICombobox(null, this.choices
, this.options
);
1209 dl
.lastElementChild
.appendChild(cbox
.render());
1212 var inputEl
= E('input', {
1213 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1215 'class': 'cbi-input-text',
1216 'placeholder': this.options
.placeholder
1219 dl
.lastElementChild
.appendChild(inputEl
);
1220 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1222 if (this.options
.datatype
)
1223 L
.ui
.addValidator(inputEl
, this.options
.datatype
,
1224 true, null, 'blur', 'keyup');
1227 for (var i
= 0; i
< this.values
.length
; i
++)
1228 this.addItem(dl
, this.values
[i
],
1229 this.choices
? this.choices
[this.values
[i
]] : null);
1231 return this.bind(dl
);
1234 bind: function(dl
) {
1235 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1236 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1237 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1241 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1242 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1244 L
.dom
.bindClassInstance(dl
, this);
1249 addItem: function(dl
, value
, text
, flash
) {
1251 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1252 E('span', {}, text
|| value
),
1255 'name': this.options
.name
,
1256 'value': value
})]);
1258 dl
.querySelectorAll('.item').forEach(function(item
) {
1262 var hidden
= item
.querySelector('input[type="hidden"]');
1264 if (hidden
&& hidden
.parentNode
!== item
)
1267 if (hidden
&& hidden
.value
=== value
)
1272 var ai
= dl
.querySelector('.add-item');
1273 ai
.parentNode
.insertBefore(new_item
, ai
);
1276 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1287 removeItem: function(dl
, item
) {
1288 var value
= item
.querySelector('input[type="hidden"]').value
;
1289 var sb
= dl
.querySelector('.cbi-dropdown');
1291 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1292 if (li
.getAttribute('data-value') === value
) {
1293 if (li
.hasAttribute('dynlistcustom'))
1294 li
.parentNode
.removeChild(li
);
1296 li
.removeAttribute('unselectable');
1300 item
.parentNode
.removeChild(item
);
1302 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1313 handleClick: function(ev
) {
1314 var dl
= ev
.currentTarget
,
1315 item
= findParent(ev
.target
, '.item');
1318 this.removeItem(dl
, item
);
1320 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1321 var input
= ev
.target
.previousElementSibling
;
1322 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1323 this.addItem(dl
, input
.value
, null, true);
1329 handleDropdownChange: function(ev
) {
1330 var dl
= ev
.currentTarget
,
1331 sbIn
= ev
.detail
.instance
,
1332 sbEl
= ev
.detail
.element
,
1333 sbVal
= ev
.detail
.value
;
1338 sbIn
.setValues(sbEl
, null);
1339 sbVal
.element
.setAttribute('unselectable', '');
1341 if (sbVal
.element
.hasAttribute('created')) {
1342 sbVal
.element
.removeAttribute('created');
1343 sbVal
.element
.setAttribute('dynlistcustom', '');
1346 this.addItem(dl
, sbVal
.value
, sbVal
.text
, true);
1349 handleKeydown: function(ev
) {
1350 var dl
= ev
.currentTarget
,
1351 item
= findParent(ev
.target
, '.item');
1354 switch (ev
.keyCode
) {
1355 case 8: /* backspace */
1356 if (item
.previousElementSibling
)
1357 item
.previousElementSibling
.focus();
1359 this.removeItem(dl
, item
);
1362 case 46: /* delete */
1363 if (item
.nextElementSibling
) {
1364 if (item
.nextElementSibling
.classList
.contains('item'))
1365 item
.nextElementSibling
.focus();
1367 item
.nextElementSibling
.firstElementChild
.focus();
1370 this.removeItem(dl
, item
);
1374 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1375 switch (ev
.keyCode
) {
1376 case 13: /* enter */
1377 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1378 this.addItem(dl
, ev
.target
.value
, null, true);
1379 ev
.target
.value
= '';
1384 ev
.preventDefault();
1390 getValue: function() {
1391 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1392 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1395 for (var i
= 0; i
< items
.length
; i
++)
1396 v
.push(items
[i
].value
);
1398 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1399 input
.classList
.contains('cbi-input-invalid') == false &&
1400 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1401 v
.push(input
.value
);
1406 setValue: function(values
) {
1407 if (!Array
.isArray(values
))
1408 values
= (values
!= null && values
!= '') ? [ values
] : [];
1410 var items
= this.node
.querySelectorAll('.item');
1412 for (var i
= 0; i
< items
.length
; i
++)
1413 if (items
[i
].parentNode
=== this.node
)
1414 this.removeItem(this.node
, items
[i
]);
1416 for (var i
= 0; i
< values
.length
; i
++)
1417 this.addItem(this.node
, values
[i
],
1418 this.choices
? this.choices
[values
[i
]] : null);
1422 var UIHiddenfield
= UIElement
.extend({
1423 __init__: function(value
, options
) {
1425 this.options
= Object
.assign({
1430 render: function() {
1431 var hiddenEl
= E('input', {
1432 'id': this.options
.id
,
1437 return this.bind(hiddenEl
);
1440 bind: function(hiddenEl
) {
1441 this.node
= hiddenEl
;
1443 L
.dom
.bindClassInstance(hiddenEl
, this);
1448 getValue: function() {
1449 return this.node
.value
;
1452 setValue: function(value
) {
1453 this.node
.value
= value
;
1457 var UIFileUpload
= UIElement
.extend({
1458 __init__: function(value
, options
) {
1460 this.options
= Object
.assign({
1462 enable_upload
: true,
1463 enable_remove
: true,
1464 root_directory
: '/etc/luci-uploads'
1468 callFileStat
: rpc
.declare({
1471 'params': [ 'path' ],
1472 'expect': { '': {} }
1475 callFileList
: rpc
.declare({
1478 'params': [ 'path' ],
1479 'expect': { 'entries': [] }
1482 callFileRemove
: rpc
.declare({
1485 'params': [ 'path' ]
1488 bind: function(browserEl
) {
1489 this.node
= browserEl
;
1491 this.setUpdateEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1492 this.setChangeEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1494 L
.dom
.bindClassInstance(browserEl
, this);
1499 render: function() {
1500 return Promise
.resolve(this.value
!= null ? this.callFileStat(this.value
) : null).then(L
.bind(function(stat
) {
1503 if (L
.isObject(stat
) && stat
.type
!= 'directory')
1506 if (this.stat
!= null)
1507 label
= [ this.iconForType(this.stat
.type
), ' %s (%1000mB)'.format(this.truncatePath(this.stat
.path
), this.stat
.size
) ];
1508 else if (this.value
!= null)
1509 label
= [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value
), _('File not accessible')) ];
1511 label
= _('Select file…');
1513 return this.bind(E('div', { 'id': this.options
.id
}, [
1516 'click': L
.ui
.createHandlerFn(this, 'handleFileBrowser')
1519 'class': 'cbi-filebrowser'
1523 'name': this.options
.name
,
1530 truncatePath: function(path
) {
1531 if (path
.length
> 50)
1532 path
= path
.substring(0, 25) + '…' + path
.substring(path
.length
- 25);
1537 iconForType: function(type
) {
1541 'src': L
.resource('cbi/link.gif'),
1542 'title': _('Symbolic link'),
1548 'src': L
.resource('cbi/folder.gif'),
1549 'title': _('Directory'),
1555 'src': L
.resource('cbi/file.gif'),
1562 canonicalizePath: function(path
) {
1563 return path
.replace(/\/{2,}/, '/')
1564 .replace(/\/\.(\/|$)/g, '/')
1565 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1566 .replace(/\/$/, '');
1569 splitPath: function(path
) {
1570 var croot
= this.canonicalizePath(this.options
.root_directory
|| '/'),
1571 cpath
= this.canonicalizePath(path
|| '/');
1573 if (cpath
.length
<= croot
.length
)
1576 if (cpath
.charAt(croot
.length
) != '/')
1579 var parts
= cpath
.substring(croot
.length
+ 1).split(/\//);
1581 parts
.unshift(croot
);
1586 handleUpload: function(path
, list
, ev
) {
1587 var form
= ev
.target
.parentNode
,
1588 fileinput
= form
.querySelector('input[type="file"]'),
1589 nameinput
= form
.querySelector('input[type="text"]'),
1590 filename
= (nameinput
.value
!= null ? nameinput
.value
: '').trim();
1592 ev
.preventDefault();
1594 if (filename
== '' || filename
.match(/\//) || fileinput
.files
[0] == null)
1597 var existing
= list
.filter(function(e
) { return e
.name
== filename
})[0];
1599 if (existing
!= null && existing
.type
== 'directory')
1600 return alert(_('A directory with the same name already exists.'));
1601 else if (existing
!= null && !confirm(_('Overwrite existing file "%s" ?').format(filename
)))
1604 var data
= new FormData();
1606 data
.append('sessionid', L
.env
.sessionid
);
1607 data
.append('filename', path
+ '/' + filename
);
1608 data
.append('filedata', fileinput
.files
[0]);
1610 return L
.Request
.post('/cgi-bin/cgi-upload', data
, {
1611 progress
: L
.bind(function(btn
, ev
) {
1612 btn
.firstChild
.data
= '%.2f%%'.format((ev
.loaded
/ ev
.total
) * 100);
1614 }).then(L
.bind(function(path
, ev
, res
) {
1615 var reply
= res
.json();
1617 if (L
.isObject(reply
) && reply
.failure
)
1618 alert(_('Upload request failed: %s').format(reply
.message
));
1620 return this.handleSelect(path
, null, ev
);
1621 }, this, path
, ev
));
1624 handleDelete: function(path
, fileStat
, ev
) {
1625 var parent
= path
.replace(/\/[^\/]+$/, '') || '/',
1626 name
= path
.replace(/^.+\//, ''),
1629 ev
.preventDefault();
1631 if (fileStat
.type
== 'directory')
1632 msg
= _('Do you really want to recursively delete the directory "%s" ?').format(name
);
1634 msg
= _('Do you really want to delete "%s" ?').format(name
);
1637 var button
= this.node
.firstElementChild
,
1638 hidden
= this.node
.lastElementChild
;
1640 if (path
== hidden
.value
) {
1641 L
.dom
.content(button
, _('Select file…'));
1645 return this.callFileRemove(path
).then(L
.bind(function(parent
, ev
, rc
) {
1647 return this.handleSelect(parent
, null, ev
);
1649 alert(_('Delete permission denied'));
1651 alert(_('Delete request failed: %d %s').format(rc
, rpc
.getStatusText(rc
)));
1653 }, this, parent
, ev
));
1657 renderUpload: function(path
, list
) {
1658 if (!this.options
.enable_upload
)
1664 'class': 'btn cbi-button-positive',
1665 'click': function(ev
) {
1666 var uploadForm
= ev
.target
.nextElementSibling
,
1667 fileInput
= uploadForm
.querySelector('input[type="file"]');
1669 ev
.target
.style
.display
= 'none';
1670 uploadForm
.style
.display
= '';
1673 }, _('Upload file…')),
1674 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1677 'style': 'display:none',
1678 'change': function(ev
) {
1679 var nameinput
= ev
.target
.parentNode
.querySelector('input[type="text"]'),
1680 uploadbtn
= ev
.target
.parentNode
.querySelector('button.cbi-button-save');
1682 nameinput
.value
= ev
.target
.value
.replace(/^.+[\/\\]/, '');
1683 uploadbtn
.disabled
= false;
1688 'click': function(ev
) {
1689 ev
.preventDefault();
1690 ev
.target
.previousElementSibling
.click();
1693 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1695 'class': 'btn cbi-button-save',
1696 'click': L
.ui
.createHandlerFn(this, 'handleUpload', path
, list
),
1698 }, _('Upload file'))
1703 renderListing: function(container
, path
, list
) {
1704 var breadcrumb
= E('p'),
1707 list
.sort(function(a
, b
) {
1708 var isDirA
= (a
.type
== 'directory'),
1709 isDirB
= (b
.type
== 'directory');
1711 if (isDirA
!= isDirB
)
1712 return isDirA
< isDirB
;
1714 return a
.name
> b
.name
;
1717 for (var i
= 0; i
< list
.length
; i
++) {
1718 if (!this.options
.show_hidden
&& list
[i
].name
.charAt(0) == '.')
1721 var entrypath
= this.canonicalizePath(path
+ '/' + list
[i
].name
),
1722 selected
= (entrypath
== this.node
.lastElementChild
.value
),
1723 mtime
= new Date(list
[i
].mtime
* 1000);
1725 rows
.appendChild(E('li', [
1726 E('div', { 'class': 'name' }, [
1727 this.iconForType(list
[i
].type
),
1731 'style': selected
? 'font-weight:bold' : null,
1732 'click': L
.ui
.createHandlerFn(this, 'handleSelect',
1733 entrypath
, list
[i
].type
!= 'directory' ? list
[i
] : null)
1734 }, '%h'.format(list
[i
].name
))
1736 E('div', { 'class': 'mtime hide-xs' }, [
1737 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1738 mtime
.getFullYear(),
1739 mtime
.getMonth() + 1,
1746 selected
? E('button', {
1748 'click': L
.ui
.createHandlerFn(this, 'handleReset')
1749 }, _('Deselect')) : '',
1750 this.options
.enable_remove
? E('button', {
1751 'class': 'btn cbi-button-negative',
1752 'click': L
.ui
.createHandlerFn(this, 'handleDelete', entrypath
, list
[i
])
1753 }, _('Delete')) : ''
1758 if (!rows
.firstElementChild
)
1759 rows
.appendChild(E('em', _('No entries in this directory')));
1761 var dirs
= this.splitPath(path
),
1764 for (var i
= 0; i
< dirs
.length
; i
++) {
1765 cur
= cur
? cur
+ '/' + dirs
[i
] : dirs
[i
];
1766 L
.dom
.append(breadcrumb
, [
1770 'click': L
.ui
.createHandlerFn(this, 'handleSelect', cur
|| '/', null)
1771 }, dirs
[i
] != '' ? '%h'.format(dirs
[i
]) : E('em', '(root)')),
1775 L
.dom
.content(container
, [
1778 E('div', { 'class': 'right' }, [
1779 this.renderUpload(path
, list
),
1783 'click': L
.ui
.createHandlerFn(this, 'handleCancel')
1789 handleCancel: function(ev
) {
1790 var button
= this.node
.firstElementChild
,
1791 browser
= button
.nextElementSibling
;
1793 browser
.classList
.remove('open');
1794 button
.style
.display
= '';
1796 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1799 handleReset: function(ev
) {
1800 var button
= this.node
.firstElementChild
,
1801 hidden
= this.node
.lastElementChild
;
1804 L
.dom
.content(button
, _('Select file…'));
1806 this.handleCancel(ev
);
1809 handleSelect: function(path
, fileStat
, ev
) {
1810 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1811 ul
= browser
.querySelector('ul');
1813 if (fileStat
== null) {
1814 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1815 this.callFileList(path
).then(L
.bind(this.renderListing
, this, browser
, path
));
1818 var button
= this.node
.firstElementChild
,
1819 hidden
= this.node
.lastElementChild
;
1821 path
= this.canonicalizePath(path
);
1823 L
.dom
.content(button
, [
1824 this.iconForType(fileStat
.type
),
1825 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1828 browser
.classList
.remove('open');
1829 button
.style
.display
= '';
1830 hidden
.value
= path
;
1832 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1833 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1837 handleFileBrowser: function(ev
) {
1838 var button
= ev
.target
,
1839 browser
= button
.nextElementSibling
,
1840 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : this.options
.root_directory
;
1842 if (this.options
.root_directory
.indexOf(path
) != 0)
1843 path
= this.options
.root_directory
;
1845 ev
.preventDefault();
1847 return this.callFileList(path
).then(L
.bind(function(button
, browser
, path
, list
) {
1848 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1849 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1852 button
.style
.display
= 'none';
1853 browser
.classList
.add('open');
1855 return this.renderListing(browser
, path
, list
);
1856 }, this, button
, browser
, path
));
1859 getValue: function() {
1860 return this.node
.lastElementChild
.value
;
1863 setValue: function(value
) {
1864 this.node
.lastElementChild
.value
= value
;
1869 return L
.Class
.extend({
1870 __init__: function() {
1871 modalDiv
= document
.body
.appendChild(
1872 L
.dom
.create('div', { id
: 'modal_overlay' },
1873 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1875 tooltipDiv
= document
.body
.appendChild(
1876 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1878 /* setup old aliases */
1879 L
.showModal
= this.showModal
;
1880 L
.hideModal
= this.hideModal
;
1881 L
.showTooltip
= this.showTooltip
;
1882 L
.hideTooltip
= this.hideTooltip
;
1883 L
.itemlist
= this.itemlist
;
1885 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1886 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1887 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1888 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1890 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1891 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1892 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1896 showModal: function(title
, children
/* , ... */) {
1897 var dlg
= modalDiv
.firstElementChild
;
1899 dlg
.setAttribute('class', 'modal');
1901 for (var i
= 2; i
< arguments
.length
; i
++)
1902 dlg
.classList
.add(arguments
[i
]);
1904 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1905 L
.dom
.append(dlg
, children
);
1907 document
.body
.classList
.add('modal-overlay-active');
1912 hideModal: function() {
1913 document
.body
.classList
.remove('modal-overlay-active');
1917 showTooltip: function(ev
) {
1918 var target
= findParent(ev
.target
, '[data-tooltip]');
1923 if (tooltipTimeout
!== null) {
1924 window
.clearTimeout(tooltipTimeout
);
1925 tooltipTimeout
= null;
1928 var rect
= target
.getBoundingClientRect(),
1929 x
= rect
.left
+ window
.pageXOffset
,
1930 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
1932 tooltipDiv
.className
= 'cbi-tooltip';
1933 tooltipDiv
.innerHTML
= '▲ ';
1934 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
1936 if (target
.hasAttribute('data-tooltip-style'))
1937 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
1939 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
1940 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
1941 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
1944 tooltipDiv
.style
.top
= y
+ 'px';
1945 tooltipDiv
.style
.left
= x
+ 'px';
1946 tooltipDiv
.style
.opacity
= 1;
1948 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
1950 detail
: { target
: target
}
1954 hideTooltip: function(ev
) {
1955 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
1956 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
1959 if (tooltipTimeout
!== null) {
1960 window
.clearTimeout(tooltipTimeout
);
1961 tooltipTimeout
= null;
1964 tooltipDiv
.style
.opacity
= 0;
1965 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
1967 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
1970 addNotification: function(title
, children
/*, ... */) {
1971 var mc
= document
.querySelector('#maincontent') || document
.body
;
1972 var msg
= E('div', {
1973 'class': 'alert-message fade-in',
1974 'style': 'display:flex',
1975 'transitionend': function(ev
) {
1976 var node
= ev
.currentTarget
;
1977 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
1978 node
.parentNode
.removeChild(node
);
1981 E('div', { 'style': 'flex:10' }),
1982 E('div', { 'style': 'flex:1; display:flex' }, [
1985 'style': 'margin-left:auto; margin-top:auto',
1986 'click': function(ev
) {
1987 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
1995 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
1997 L
.dom
.append(msg
.firstElementChild
, children
);
1999 for (var i
= 2; i
< arguments
.length
; i
++)
2000 msg
.classList
.add(arguments
[i
]);
2002 mc
.insertBefore(msg
, mc
.firstElementChild
);
2008 itemlist: function(node
, items
, separators
) {
2011 if (!Array
.isArray(separators
))
2012 separators
= [ separators
|| E('br') ];
2014 for (var i
= 0; i
< items
.length
; i
+= 2) {
2015 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
2016 var sep
= separators
[(i
/2) % separators
.length
],
2019 children
.push(E('span', { class: 'nowrap' }, [
2020 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2024 if ((i
+2) < items
.length
)
2025 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2029 L
.dom
.content(node
, children
);
2035 tabs
: L
.Class
.singleton({
2037 var groups
= [], prevGroup
= null, currGroup
= null;
2039 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2040 var parent
= tab
.parentNode
;
2042 if (!parent
.hasAttribute('data-tab-group'))
2043 parent
.setAttribute('data-tab-group', groups
.length
);
2045 currGroup
= +parent
.getAttribute('data-tab-group');
2047 if (currGroup
!== prevGroup
) {
2048 prevGroup
= currGroup
;
2050 if (!groups
[currGroup
])
2051 groups
[currGroup
] = [];
2054 groups
[currGroup
].push(tab
);
2057 for (var i
= 0; i
< groups
.length
; i
++)
2058 this.initTabGroup(groups
[i
]);
2060 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2065 initTabGroup: function(panes
) {
2066 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2069 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2070 group
= panes
[0].parentNode
,
2071 groupId
= +group
.getAttribute('data-tab-group'),
2074 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2075 var name
= pane
.getAttribute('data-tab'),
2076 title
= pane
.getAttribute('data-tab-title'),
2077 active
= pane
.getAttribute('data-tab-active') === 'true';
2079 menu
.appendChild(E('li', {
2080 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2081 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2085 'click': this.switchTab
.bind(this)
2092 group
.parentNode
.insertBefore(menu
, group
);
2094 if (selected
=== null) {
2095 selected
= this.getActiveTabId(panes
[0]);
2097 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2098 for (var i
= 0; i
< panes
.length
; i
++) {
2099 if (!this.isEmptyPane(panes
[i
])) {
2106 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2107 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2108 panes
[selected
].setAttribute('data-tab-active', 'true');
2110 this.setActiveTabId(panes
[selected
], selected
);
2113 this.updateTabs(group
);
2116 isEmptyPane: function(pane
) {
2117 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2120 getPathForPane: function(pane
) {
2121 var path
= [], node
= null;
2123 for (node
= pane
? pane
.parentNode
: null;
2124 node
!= null && node
.hasAttribute
!= null;
2125 node
= node
.parentNode
)
2127 if (node
.hasAttribute('data-tab'))
2128 path
.unshift(node
.getAttribute('data-tab'));
2129 else if (node
.hasAttribute('data-section-id'))
2130 path
.unshift(node
.getAttribute('data-section-id'));
2133 return path
.join('/');
2136 getActiveTabState: function() {
2137 var page
= document
.body
.getAttribute('data-page');
2140 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2141 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2146 window
.sessionStorage
.removeItem('tab');
2147 return { page
: page
, paths
: {} };
2150 getActiveTabId: function(pane
) {
2151 var path
= this.getPathForPane(pane
);
2152 return +this.getActiveTabState().paths
[path
] || 0;
2155 setActiveTabId: function(pane
, tabIndex
) {
2156 var path
= this.getPathForPane(pane
);
2159 var state
= this.getActiveTabState();
2160 state
.paths
[path
] = tabIndex
;
2162 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2164 catch (e
) { return false; }
2169 updateTabs: function(ev
, root
) {
2170 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2171 var menu
= pane
.parentNode
.previousElementSibling
,
2172 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2173 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2178 if (this.isEmptyPane(pane
)) {
2179 tab
.style
.display
= 'none';
2180 tab
.classList
.remove('flash');
2182 else if (tab
.style
.display
=== 'none') {
2183 tab
.style
.display
= '';
2184 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2188 tab
.setAttribute('data-errors', n_errors
);
2189 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2190 tab
.setAttribute('data-tooltip-style', 'error');
2193 tab
.removeAttribute('data-errors');
2194 tab
.removeAttribute('data-tooltip');
2199 switchTab: function(ev
) {
2200 var tab
= ev
.target
.parentNode
,
2201 name
= tab
.getAttribute('data-tab'),
2202 menu
= tab
.parentNode
,
2203 group
= menu
.nextElementSibling
,
2204 groupId
= +group
.getAttribute('data-tab-group'),
2207 ev
.preventDefault();
2209 if (!tab
.classList
.contains('cbi-tab-disabled'))
2212 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2213 tab
.classList
.remove('cbi-tab');
2214 tab
.classList
.remove('cbi-tab-disabled');
2216 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2219 group
.childNodes
.forEach(function(pane
) {
2220 if (L
.dom
.matches(pane
, '[data-tab]')) {
2221 if (pane
.getAttribute('data-tab') === name
) {
2222 pane
.setAttribute('data-tab-active', 'true');
2223 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2226 pane
.setAttribute('data-tab-active', 'false');
2236 changes
: L
.Class
.singleton({
2238 if (!L
.env
.sessionid
)
2241 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2244 setIndicator: function(n
) {
2245 var i
= document
.querySelector('.uci_change_indicator');
2247 var poll
= document
.getElementById('xhr_poll_status');
2248 i
= poll
.parentNode
.insertBefore(E('a', {
2250 'class': 'uci_change_indicator label notice',
2251 'click': L
.bind(this.displayChanges
, this)
2256 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2257 i
.classList
.add('flash');
2258 i
.style
.display
= '';
2261 i
.classList
.remove('flash');
2262 i
.style
.display
= 'none';
2266 renderChangeIndicator: function(changes
) {
2269 for (var config
in changes
)
2270 if (changes
.hasOwnProperty(config
))
2271 n_changes
+= changes
[config
].length
;
2273 this.changes
= changes
;
2274 this.setIndicator(n_changes
);
2278 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2279 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2280 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2281 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2282 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2283 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2284 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2285 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2286 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2287 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2290 displayChanges: function() {
2291 var list
= E('div', { 'class': 'uci-change-list' }),
2292 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2293 E('div', { 'class': 'cbi-section' }, [
2294 E('strong', _('Legend:')),
2295 E('div', { 'class': 'uci-change-legend' }, [
2296 E('div', { 'class': 'uci-change-legend-label' }, [
2297 E('ins', ' '), ' ', _('Section added') ]),
2298 E('div', { 'class': 'uci-change-legend-label' }, [
2299 E('del', ' '), ' ', _('Section removed') ]),
2300 E('div', { 'class': 'uci-change-legend-label' }, [
2301 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2302 E('div', { 'class': 'uci-change-legend-label' }, [
2303 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2305 E('div', { 'class': 'right' }, [
2309 'click': L
.ui
.hideModal
,
2310 'value': _('Dismiss')
2314 'class': 'cbi-button cbi-button-positive important',
2315 'click': L
.bind(this.apply
, this, true),
2316 'value': _('Save & Apply')
2320 'class': 'cbi-button cbi-button-reset',
2321 'click': L
.bind(this.revert
, this),
2322 'value': _('Revert')
2326 for (var config
in this.changes
) {
2327 if (!this.changes
.hasOwnProperty(config
))
2330 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2332 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2333 var chg
= this.changes
[config
][i
],
2334 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2336 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2342 if (added
!= null && chg
[1] == added
[0])
2343 return '@' + added
[1] + '[-1]';
2348 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2355 if (chg[0] == 'add')
2356 added = [ chg[1], chg[2] ];
2360 list.appendChild(E('br'));
2361 dlg.classList.add('uci-dialog');
2364 displayStatus: function(type, content) {
2366 var message = L.ui.showModal('', '');
2368 message.classList.add('alert-message');
2369 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2372 L.dom.content(message, content);
2374 if (!this.was_polling) {
2375 this.was_polling = L.Request.poll.active();
2376 L.Request.poll.stop();
2382 if (this.was_polling)
2383 L.Request.poll.start();
2387 rollback: function(checked) {
2389 this.displayStatus('warning spinning',
2390 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2391 .format(L.env.apply_rollback)));
2393 var call = function(r, data, duration) {
2394 if (r.status === 204) {
2395 L.ui.changes.displayStatus('warning', [
2396 E('h4', _('Configuration has been rolled back!')),
2397 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)),
2398 E('div', { 'class': 'right' }, [
2402 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
2403 'value': _('Dismiss')
2407 'class': 'btn cbi-button-action important',
2408 'click': L.bind(L.ui.changes.revert, L.ui.changes),
2409 'value': _('Revert changes')
2413 'class': 'btn cbi-button-negative important',
2414 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
2415 'value': _('Apply unchecked')
2423 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2424 window.setTimeout(function() {
2425 L.Request.request(L.url('admin/uci/confirm'), {
2427 timeout: L.env.apply_timeout * 1000,
2428 query: { sid: L.env.sessionid, token: L.env.token }
2433 call({ status: 0 });
2436 this.displayStatus('warning', [
2437 E('h4', _('Device unreachable!')),
2438 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.'))
2443 confirm: function(checked, deadline, override_token) {
2445 var ts = Date.now();
2447 this.displayStatus('notice');
2450 this.confirm_auth = { token: override_token };
2452 var call = function(r, data, duration) {
2453 if (Date.now() >= deadline) {
2454 window.clearTimeout(tt);
2455 L.ui.changes.rollback(checked);
2458 else if (r && (r.status === 200 || r.status === 204)) {
2459 document.dispatchEvent(new CustomEvent('uci-applied'));
2461 L.ui.changes.setIndicator(0);
2462 L.ui.changes.displayStatus('notice',
2463 E('p', _('Configuration has been applied.')));
2465 window.clearTimeout(tt);
2466 window.setTimeout(function() {
2467 //L.ui.changes.displayStatus(false);
2468 window.location = window.location.href.split('#')[0];
2469 }, L.env.apply_display * 1000);
2474 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2475 window.setTimeout(function() {
2476 L.Request.request(L.url('admin/uci/confirm'), {
2478 timeout: L.env.apply_timeout * 1000,
2479 query: L.ui.changes.confirm_auth
2480 }).then(call, call);
2484 var tick = function() {
2485 var now = Date.now();
2487 L.ui.changes.displayStatus('notice spinning',
2488 E('p', _('Waiting for configuration to get applied… %ds')
2489 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2491 if (now >= deadline)
2494 tt = window.setTimeout(tick, 1000 - (now - ts));
2500 /* wait a few seconds for the settings to become effective */
2501 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2504 apply: function(checked) {
2505 this.displayStatus('notice spinning',
2506 E('p', _('Starting configuration apply…')));
2508 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2510 query: { sid: L.env.sessionid, token: L.env.token }
2511 }).then(function(r) {
2512 if (r.status === (checked ? 200 : 204)) {
2513 var tok = null; try { tok = r.json(); } catch(e) {}
2514 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2515 L.ui.changes.confirm_auth = tok;
2517 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2519 else if (checked && r.status === 204) {
2520 L.ui.changes.displayStatus('notice',
2521 E('p', _('There are no changes to apply')));
2523 window.setTimeout(function() {
2524 L.ui.changes.displayStatus(false);
2525 }, L.env.apply_display * 1000);
2528 L.ui.changes.displayStatus('warning',
2529 E('p', _('Apply request failed with status <code>%h</code>')
2530 .format(r.responseText || r.statusText || r.status)));
2532 window.setTimeout(function() {
2533 L.ui.changes.displayStatus(false);
2534 }, L.env.apply_display * 1000);
2539 revert: function() {
2540 this.displayStatus('notice spinning',
2541 E('p', _('Reverting configuration…')));
2543 L.Request.request(L.url('admin/uci/revert'), {
2545 query: { sid: L.env.sessionid, token: L.env.token }
2546 }).then(function(r) {
2547 if (r.status === 200) {
2548 document.dispatchEvent(new CustomEvent('uci-reverted'));
2550 L.ui.changes.setIndicator(0);
2551 L.ui.changes.displayStatus('notice',
2552 E('p', _('Changes have been reverted.')));
2554 window.setTimeout(function() {
2555 //L.ui.changes.displayStatus(false);
2556 window.location = window.location.href.split('#')[0];
2557 }, L.env.apply_display * 1000);
2560 L.ui.changes.displayStatus('warning',
2561 E('p', _('Revert request failed with status <code>%h</code>')
2562 .format(r.statusText || r.status)));
2564 window.setTimeout(function() {
2565 L.ui.changes.displayStatus(false);
2566 }, L.env.apply_display * 1000);
2572 addValidator: function(field, type, optional, vfunc /*, ... */) {
2576 var events = this.varargs(arguments, 3);
2577 if (events.length == 0)
2578 events.push('blur', 'keyup');
2581 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2582 validatorFn = cbiValidator.validate.bind(cbiValidator);
2584 for (var i = 0; i < events.length; i++)
2585 field.addEventListener(events[i], validatorFn);
2594 createHandlerFn: function(ctx, fn /*, ... */) {
2595 if (typeof(fn) == 'string')
2598 if (typeof(fn) != 'function')
2601 return Function.prototype.bind.apply(function() {
2602 var t = arguments[arguments.length - 1].target;
2604 t.classList.add('spinning');
2610 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2611 t.classList.remove('spinning');
2614 }, this.varargs(arguments, 2, ctx));
2618 Textfield: UITextfield,
2619 Textarea: UITextarea,
2620 Checkbox: UICheckbox,
2622 Dropdown: UIDropdown,
2623 DynamicList: UIDynamicList,
2624 Combobox: UICombobox,
2625 Hiddenfield: UIHiddenfield,
2626 FileUpload: UIFileUpload