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 var tag_changed
= L
.bind(function(ev
) { this.setAttribute('data-changed', true) }, this.node
);
75 for (var i
= 1; i
< arguments
.length
; i
++)
76 targetNode
.addEventListener(arguments
[i
], tag_changed
);
78 this.registerEvents(targetNode
, 'widget-change', this.varargs(arguments
, 1));
82 var UITextfield
= UIElement
.extend({
83 __init__: function(value
, options
) {
85 this.options
= Object
.assign({
92 var frameEl
= E('div', { 'id': this.options
.id
});
94 if (this.options
.password
) {
95 frameEl
.classList
.add('nowrap');
96 frameEl
.appendChild(E('input', {
98 'style': 'position:absolute; left:-100000px',
101 'name': this.options
.name
? 'password.%s'.format(this.options
.name
) : null
105 frameEl
.appendChild(E('input', {
106 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
107 'name': this.options
.name
,
108 'type': this.options
.password
? 'password' : 'text',
109 'class': this.options
.password
? 'cbi-input-password' : 'cbi-input-text',
110 'readonly': this.options
.readonly
? '' : null,
111 'maxlength': this.options
.maxlength
,
112 'placeholder': this.options
.placeholder
,
116 if (this.options
.password
)
117 frameEl
.appendChild(E('button', {
118 'class': 'cbi-button cbi-button-neutral',
119 'title': _('Reveal/hide password'),
120 'aria-label': _('Reveal/hide password'),
121 'click': function(ev
) {
122 var e
= this.previousElementSibling
;
123 e
.type
= (e
.type
=== 'password') ? 'text' : 'password';
128 return this.bind(frameEl
);
131 bind: function(frameEl
) {
132 var inputEl
= frameEl
.childNodes
[+!!this.options
.password
];
136 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
137 this.setChangeEvents(inputEl
, 'change');
139 L
.dom
.bindClassInstance(frameEl
, this);
144 getValue: function() {
145 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
146 return inputEl
.value
;
149 setValue: function(value
) {
150 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
151 inputEl
.value
= value
;
155 var UITextarea
= UIElement
.extend({
156 __init__: function(value
, options
) {
158 this.options
= Object
.assign({
167 var frameEl
= E('div', { 'id': this.options
.id
}),
168 value
= (this.value
!= null) ? String(this.value
) : '';
170 frameEl
.appendChild(E('textarea', {
171 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
172 'name': this.options
.name
,
173 'class': 'cbi-input-textarea',
174 'readonly': this.options
.readonly
? '' : null,
175 'placeholder': this.options
.placeholder
,
176 'style': !this.options
.cols
? 'width:100%' : null,
177 'cols': this.options
.cols
,
178 'rows': this.options
.rows
,
179 'wrap': this.options
.wrap
? '' : null
182 if (this.options
.monospace
)
183 frameEl
.firstElementChild
.style
.fontFamily
= 'monospace';
185 return this.bind(frameEl
);
188 bind: function(frameEl
) {
189 var inputEl
= frameEl
.firstElementChild
;
193 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
194 this.setChangeEvents(inputEl
, 'change');
196 L
.dom
.bindClassInstance(frameEl
, this);
201 getValue: function() {
202 return this.node
.firstElementChild
.value
;
205 setValue: function(value
) {
206 this.node
.firstElementChild
.value
= value
;
210 var UICheckbox
= UIElement
.extend({
211 __init__: function(value
, options
) {
213 this.options
= Object
.assign({
220 var frameEl
= E('div', {
221 'id': this.options
.id
,
222 'class': 'cbi-checkbox'
225 if (this.options
.hiddenname
)
226 frameEl
.appendChild(E('input', {
228 'name': this.options
.hiddenname
,
232 frameEl
.appendChild(E('input', {
233 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
234 'name': this.options
.name
,
236 'value': this.options
.value_enabled
,
237 'checked': (this.value
== this.options
.value_enabled
) ? '' : null
240 return this.bind(frameEl
);
243 bind: function(frameEl
) {
246 this.setUpdateEvents(frameEl
.lastElementChild
, 'click', 'blur');
247 this.setChangeEvents(frameEl
.lastElementChild
, 'change');
249 L
.dom
.bindClassInstance(frameEl
, this);
254 isChecked: function() {
255 return this.node
.lastElementChild
.checked
;
258 getValue: function() {
259 return this.isChecked()
260 ? this.options
.value_enabled
261 : this.options
.value_disabled
;
264 setValue: function(value
) {
265 this.node
.lastElementChild
.checked
= (value
== this.options
.value_enabled
);
269 var UISelect
= UIElement
.extend({
270 __init__: function(value
, choices
, options
) {
271 if (!L
.isObject(choices
))
274 if (!Array
.isArray(value
))
275 value
= (value
!= null && value
!= '') ? [ value
] : [];
277 if (!options
.multiple
&& value
.length
> 1)
281 this.choices
= choices
;
282 this.options
= Object
.assign({
285 orientation
: 'horizontal'
288 if (this.choices
.hasOwnProperty(''))
289 this.options
.optional
= true;
293 var frameEl
= E('div', { 'id': this.options
.id
}),
294 keys
= Object
.keys(this.choices
);
296 if (this.options
.sort
=== true)
298 else if (Array
.isArray(this.options
.sort
))
299 keys
= this.options
.sort
;
301 if (this.options
.widget
== 'select') {
302 frameEl
.appendChild(E('select', {
303 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
304 'name': this.options
.name
,
305 'size': this.options
.size
,
306 'class': 'cbi-input-select',
307 'multiple': this.options
.multiple
? '' : null
310 if (this.options
.optional
)
311 frameEl
.lastChild
.appendChild(E('option', {
313 'selected': (this.values
.length
== 0 || this.values
[0] == '') ? '' : null
314 }, this.choices
[''] || this.options
.placeholder
|| _('-- Please choose --')));
316 for (var i
= 0; i
< keys
.length
; i
++) {
317 if (keys
[i
] == null || keys
[i
] == '')
320 frameEl
.lastChild
.appendChild(E('option', {
322 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
323 }, this.choices
[keys
[i
]] || keys
[i
]));
327 var brEl
= (this.options
.orientation
=== 'horizontal') ? document
.createTextNode(' ') : E('br');
329 for (var i
= 0; i
< keys
.length
; i
++) {
330 frameEl
.appendChild(E('label', {}, [
332 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
333 'name': this.options
.id
|| this.options
.name
,
334 'type': this.options
.multiple
? 'checkbox' : 'radio',
335 'class': this.options
.multiple
? 'cbi-input-checkbox' : 'cbi-input-radio',
337 'checked': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
339 this.choices
[keys
[i
]] || keys
[i
]
342 if (i
+ 1 == this.options
.size
)
343 frameEl
.appendChild(brEl
);
347 return this.bind(frameEl
);
350 bind: function(frameEl
) {
353 if (this.options
.widget
== 'select') {
354 this.setUpdateEvents(frameEl
.firstChild
, 'change', 'click', 'blur');
355 this.setChangeEvents(frameEl
.firstChild
, 'change');
358 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
359 for (var i
= 0; i
< radioEls
.length
; i
++) {
360 this.setUpdateEvents(radioEls
[i
], 'change', 'click', 'blur');
361 this.setChangeEvents(radioEls
[i
], 'change', 'click', 'blur');
365 L
.dom
.bindClassInstance(frameEl
, this);
370 getValue: function() {
371 if (this.options
.widget
== 'select')
372 return this.node
.firstChild
.value
;
374 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
375 for (var i
= 0; i
< radioEls
.length
; i
++)
376 if (radioEls
[i
].checked
)
377 return radioEls
[i
].value
;
382 setValue: function(value
) {
383 if (this.options
.widget
== 'select') {
387 for (var i
= 0; i
< this.node
.firstChild
.options
.length
; i
++)
388 this.node
.firstChild
.options
[i
].selected
= (this.node
.firstChild
.options
[i
].value
== value
);
393 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
394 for (var i
= 0; i
< radioEls
.length
; i
++)
395 radioEls
[i
].checked
= (radioEls
[i
].value
== value
);
399 var UIDropdown
= UIElement
.extend({
400 __init__: function(value
, choices
, options
) {
401 if (typeof(choices
) != 'object')
404 if (!Array
.isArray(value
))
405 this.values
= (value
!= null && value
!= '') ? [ value
] : [];
409 this.choices
= choices
;
410 this.options
= Object
.assign({
412 multiple
: Array
.isArray(value
),
414 select_placeholder
: _('-- Please choose --'),
415 custom_placeholder
: _('-- custom --'),
419 create_query
: '.create-item-input',
420 create_template
: 'script[type="item-template"]'
426 'id': this.options
.id
,
427 'class': 'cbi-dropdown',
428 'multiple': this.options
.multiple
? '' : null,
429 'optional': this.options
.optional
? '' : null,
432 var keys
= Object
.keys(this.choices
);
434 if (this.options
.sort
=== true)
436 else if (Array
.isArray(this.options
.sort
))
437 keys
= this.options
.sort
;
439 if (this.options
.create
)
440 for (var i
= 0; i
< this.values
.length
; i
++)
441 if (!this.choices
.hasOwnProperty(this.values
[i
]))
442 keys
.push(this.values
[i
]);
444 for (var i
= 0; i
< keys
.length
; i
++)
445 sb
.lastElementChild
.appendChild(E('li', {
446 'data-value': keys
[i
],
447 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
448 }, this.choices
[keys
[i
]] || keys
[i
]));
450 if (this.options
.create
) {
451 var createEl
= E('input', {
453 'class': 'create-item-input',
454 'readonly': this.options
.readonly
? '' : null,
455 'maxlength': this.options
.maxlength
,
456 'placeholder': this.options
.custom_placeholder
|| this.options
.placeholder
459 if (this.options
.datatype
)
460 L
.ui
.addValidator(createEl
, this.options
.datatype
,
461 true, null, 'blur', 'keyup');
463 sb
.lastElementChild
.appendChild(E('li', { 'data-value': '-' }, createEl
));
466 if (this.options
.create_markup
)
467 sb
.appendChild(E('script', { type
: 'item-template' },
468 this.options
.create_markup
));
470 return this.bind(sb
);
474 var o
= this.options
;
476 o
.multiple
= sb
.hasAttribute('multiple');
477 o
.optional
= sb
.hasAttribute('optional');
478 o
.placeholder
= sb
.getAttribute('placeholder') || o
.placeholder
;
479 o
.display_items
= parseInt(sb
.getAttribute('display-items') || o
.display_items
);
480 o
.dropdown_items
= parseInt(sb
.getAttribute('dropdown-items') || o
.dropdown_items
);
481 o
.create_query
= sb
.getAttribute('item-create') || o
.create_query
;
482 o
.create_template
= sb
.getAttribute('item-template') || o
.create_template
;
484 var ul
= sb
.querySelector('ul'),
485 more
= sb
.appendChild(E('span', { class: 'more', tabindex
: -1 }, '···')),
486 open
= sb
.appendChild(E('span', { class: 'open', tabindex
: -1 }, '▾')),
487 canary
= sb
.appendChild(E('div')),
488 create
= sb
.querySelector(this.options
.create_query
),
489 ndisplay
= this.options
.display_items
,
492 if (this.options
.multiple
) {
493 var items
= ul
.querySelectorAll('li');
495 for (var i
= 0; i
< items
.length
; i
++) {
496 this.transformItem(sb
, items
[i
]);
498 if (items
[i
].hasAttribute('selected') && ndisplay
-- > 0)
499 items
[i
].setAttribute('display', n
++);
503 if (this.options
.optional
&& !ul
.querySelector('li[data-value=""]')) {
504 var placeholder
= E('li', { placeholder
: '' },
505 this.options
.select_placeholder
|| this.options
.placeholder
);
508 ? ul
.insertBefore(placeholder
, ul
.firstChild
)
509 : ul
.appendChild(placeholder
);
512 var items
= ul
.querySelectorAll('li'),
513 sel
= sb
.querySelectorAll('[selected]');
515 sel
.forEach(function(s
) {
516 s
.removeAttribute('selected');
519 var s
= sel
[0] || items
[0];
521 s
.setAttribute('selected', '');
522 s
.setAttribute('display', n
++);
528 this.saveValues(sb
, ul
);
530 ul
.setAttribute('tabindex', -1);
531 sb
.setAttribute('tabindex', 0);
534 sb
.setAttribute('more', '')
536 sb
.removeAttribute('more');
538 if (ndisplay
== this.options
.display_items
)
539 sb
.setAttribute('empty', '')
541 sb
.removeAttribute('empty');
543 L
.dom
.content(more
, (ndisplay
== this.options
.display_items
)
544 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
547 sb
.addEventListener('click', this.handleClick
.bind(this));
548 sb
.addEventListener('keydown', this.handleKeydown
.bind(this));
549 sb
.addEventListener('cbi-dropdown-close', this.handleDropdownClose
.bind(this));
550 sb
.addEventListener('cbi-dropdown-select', this.handleDropdownSelect
.bind(this));
552 if ('ontouchstart' in window
) {
553 sb
.addEventListener('touchstart', function(ev
) { ev
.stopPropagation(); });
554 window
.addEventListener('touchstart', this.closeAllDropdowns
);
557 sb
.addEventListener('mouseover', this.handleMouseover
.bind(this));
558 sb
.addEventListener('focus', this.handleFocus
.bind(this));
560 canary
.addEventListener('focus', this.handleCanaryFocus
.bind(this));
562 window
.addEventListener('mouseover', this.setFocus
);
563 window
.addEventListener('click', this.closeAllDropdowns
);
567 create
.addEventListener('keydown', this.handleCreateKeydown
.bind(this));
568 create
.addEventListener('focus', this.handleCreateFocus
.bind(this));
569 create
.addEventListener('blur', this.handleCreateBlur
.bind(this));
571 var li
= findParent(create
, 'li');
573 li
.setAttribute('unselectable', '');
574 li
.addEventListener('click', this.handleCreateClick
.bind(this));
579 this.setUpdateEvents(sb
, 'cbi-dropdown-open', 'cbi-dropdown-close');
580 this.setChangeEvents(sb
, 'cbi-dropdown-change', 'cbi-dropdown-close');
582 L
.dom
.bindClassInstance(sb
, this);
587 openDropdown: function(sb
) {
588 var st
= window
.getComputedStyle(sb
, null),
589 ul
= sb
.querySelector('ul'),
590 li
= ul
.querySelectorAll('li'),
591 fl
= findParent(sb
, '.cbi-value-field'),
592 sel
= ul
.querySelector('[selected]'),
593 rect
= sb
.getBoundingClientRect(),
594 items
= Math
.min(this.options
.dropdown_items
, li
.length
);
596 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
597 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
600 sb
.setAttribute('open', '');
602 var pv
= ul
.cloneNode(true);
603 pv
.classList
.add('preview');
606 fl
.classList
.add('cbi-dropdown-open');
608 if ('ontouchstart' in window
) {
609 var vpWidth
= Math
.max(document
.documentElement
.clientWidth
, window
.innerWidth
|| 0),
610 vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
611 scrollFrom
= window
.pageYOffset
,
612 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
615 ul
.style
.top
= sb
.offsetHeight
+ 'px';
616 ul
.style
.left
= -rect
.left
+ 'px';
617 ul
.style
.right
= (rect
.right
- vpWidth
) + 'px';
618 ul
.style
.maxHeight
= (vpHeight
* 0.5) + 'px';
619 ul
.style
.WebkitOverflowScrolling
= 'touch';
621 var scrollStep = function(timestamp
) {
624 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
627 var duration
= Math
.max(timestamp
- start
, 1);
628 if (duration
< 100) {
629 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
630 window
.requestAnimationFrame(scrollStep
);
633 document
.body
.scrollTop
= scrollTo
;
637 window
.requestAnimationFrame(scrollStep
);
640 ul
.style
.maxHeight
= '1px';
641 ul
.style
.top
= ul
.style
.bottom
= '';
643 window
.requestAnimationFrame(function() {
644 var itemHeight
= li
[Math
.max(0, li
.length
- 2)].getBoundingClientRect().height
,
646 spaceAbove
= rect
.top
,
647 spaceBelow
= window
.innerHeight
- rect
.height
- rect
.top
;
649 for (var i
= 0; i
< (items
== -1 ? li
.length
: items
); i
++)
650 fullHeight
+= li
[i
].getBoundingClientRect().height
;
652 if (fullHeight
<= spaceBelow
) {
653 ul
.style
.top
= rect
.height
+ 'px';
654 ul
.style
.maxHeight
= spaceBelow
+ 'px';
656 else if (fullHeight
<= spaceAbove
) {
657 ul
.style
.bottom
= rect
.height
+ 'px';
658 ul
.style
.maxHeight
= spaceAbove
+ 'px';
660 else if (spaceBelow
>= spaceAbove
) {
661 ul
.style
.top
= rect
.height
+ 'px';
662 ul
.style
.maxHeight
= (spaceBelow
- (spaceBelow
% itemHeight
)) + 'px';
665 ul
.style
.bottom
= rect
.height
+ 'px';
666 ul
.style
.maxHeight
= (spaceAbove
- (spaceAbove
% itemHeight
)) + 'px';
669 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
673 var cboxes
= ul
.querySelectorAll('[selected] input[type="checkbox"]');
674 for (var i
= 0; i
< cboxes
.length
; i
++) {
675 cboxes
[i
].checked
= true;
676 cboxes
[i
].disabled
= (cboxes
.length
== 1 && !this.options
.optional
);
679 ul
.classList
.add('dropdown');
681 sb
.insertBefore(pv
, ul
.nextElementSibling
);
683 li
.forEach(function(l
) {
684 l
.setAttribute('tabindex', 0);
687 sb
.lastElementChild
.setAttribute('tabindex', 0);
689 this.setFocus(sb
, sel
|| li
[0], true);
692 closeDropdown: function(sb
, no_focus
) {
693 if (!sb
.hasAttribute('open'))
696 var pv
= sb
.querySelector('ul.preview'),
697 ul
= sb
.querySelector('ul.dropdown'),
698 li
= ul
.querySelectorAll('li'),
699 fl
= findParent(sb
, '.cbi-value-field');
701 li
.forEach(function(l
) { l
.removeAttribute('tabindex'); });
702 sb
.lastElementChild
.removeAttribute('tabindex');
705 sb
.removeAttribute('open');
706 sb
.style
.width
= sb
.style
.height
= '';
708 ul
.classList
.remove('dropdown');
709 ul
.style
.top
= ul
.style
.bottom
= ul
.style
.maxHeight
= '';
712 fl
.classList
.remove('cbi-dropdown-open');
715 this.setFocus(sb
, sb
);
717 this.saveValues(sb
, ul
);
720 toggleItem: function(sb
, li
, force_state
) {
721 if (li
.hasAttribute('unselectable'))
724 if (this.options
.multiple
) {
725 var cbox
= li
.querySelector('input[type="checkbox"]'),
726 items
= li
.parentNode
.querySelectorAll('li'),
727 label
= sb
.querySelector('ul.preview'),
728 sel
= li
.parentNode
.querySelectorAll('[selected]').length
,
729 more
= sb
.querySelector('.more'),
730 ndisplay
= this.options
.display_items
,
733 if (li
.hasAttribute('selected')) {
734 if (force_state
!== true) {
735 if (sel
> 1 || this.options
.optional
) {
736 li
.removeAttribute('selected');
737 cbox
.checked
= cbox
.disabled
= false;
741 cbox
.disabled
= true;
746 if (force_state
!== false) {
747 li
.setAttribute('selected', '');
749 cbox
.disabled
= false;
754 while (label
&& label
.firstElementChild
)
755 label
.removeChild(label
.firstElementChild
);
757 for (var i
= 0; i
< items
.length
; i
++) {
758 items
[i
].removeAttribute('display');
759 if (items
[i
].hasAttribute('selected')) {
760 if (ndisplay
-- > 0) {
761 items
[i
].setAttribute('display', n
++);
763 label
.appendChild(items
[i
].cloneNode(true));
765 var c
= items
[i
].querySelector('input[type="checkbox"]');
767 c
.disabled
= (sel
== 1 && !this.options
.optional
);
772 sb
.setAttribute('more', '');
774 sb
.removeAttribute('more');
776 if (ndisplay
=== this.options
.display_items
)
777 sb
.setAttribute('empty', '');
779 sb
.removeAttribute('empty');
781 L
.dom
.content(more
, (ndisplay
=== this.options
.display_items
)
782 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
785 var sel
= li
.parentNode
.querySelector('[selected]');
787 sel
.removeAttribute('display');
788 sel
.removeAttribute('selected');
791 li
.setAttribute('display', 0);
792 li
.setAttribute('selected', '');
794 this.closeDropdown(sb
, true);
797 this.saveValues(sb
, li
.parentNode
);
800 transformItem: function(sb
, li
) {
801 var cbox
= E('form', {}, E('input', { type
: 'checkbox', tabindex
: -1, onclick
: 'event.preventDefault()' })),
804 while (li
.firstChild
)
805 label
.appendChild(li
.firstChild
);
807 li
.appendChild(cbox
);
808 li
.appendChild(label
);
811 saveValues: function(sb
, ul
) {
812 var sel
= ul
.querySelectorAll('li[selected]'),
813 div
= sb
.lastElementChild
,
814 name
= this.options
.name
,
818 while (div
.lastElementChild
)
819 div
.removeChild(div
.lastElementChild
);
821 sel
.forEach(function (s
) {
822 if (s
.hasAttribute('placeholder'))
827 value
: s
.hasAttribute('data-value') ? s
.getAttribute('data-value') : s
.innerText
,
831 div
.appendChild(E('input', {
839 strval
+= strval
.length
? ' ' + v
.value
: v
.value
;
847 if (this.options
.multiple
)
848 detail
.values
= values
;
850 detail
.value
= values
.length
? values
[0] : null;
854 sb
.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
860 setValues: function(sb
, values
) {
861 var ul
= sb
.querySelector('ul');
863 if (this.options
.create
) {
864 for (var value
in values
) {
865 this.createItems(sb
, value
);
867 if (!this.options
.multiple
)
872 if (this.options
.multiple
) {
873 var lis
= ul
.querySelectorAll('li[data-value]');
874 for (var i
= 0; i
< lis
.length
; i
++) {
875 var value
= lis
[i
].getAttribute('data-value');
876 if (values
=== null || !(value
in values
))
877 this.toggleItem(sb
, lis
[i
], false);
879 this.toggleItem(sb
, lis
[i
], true);
883 var ph
= ul
.querySelector('li[placeholder]');
885 this.toggleItem(sb
, ph
);
887 var lis
= ul
.querySelectorAll('li[data-value]');
888 for (var i
= 0; i
< lis
.length
; i
++) {
889 var value
= lis
[i
].getAttribute('data-value');
890 if (values
!== null && (value
in values
))
891 this.toggleItem(sb
, lis
[i
]);
896 setFocus: function(sb
, elem
, scroll
) {
897 if (sb
&& sb
.hasAttribute
&& sb
.hasAttribute('locked-in'))
900 if (sb
.target
&& findParent(sb
.target
, 'ul.dropdown'))
903 document
.querySelectorAll('.focus').forEach(function(e
) {
904 if (!matchesElem(e
, 'input')) {
905 e
.classList
.remove('focus');
912 elem
.classList
.add('focus');
915 elem
.parentNode
.scrollTop
= elem
.offsetTop
- elem
.parentNode
.offsetTop
;
919 createItems: function(sb
, value
) {
921 val
= (value
|| '').trim(),
922 ul
= sb
.querySelector('ul');
924 if (!sbox
.options
.multiple
)
925 val
= val
.length
? [ val
] : [];
927 val
= val
.length
? val
.split(/\s+/) : [];
929 val
.forEach(function(item
) {
932 ul
.childNodes
.forEach(function(li
) {
933 if (li
.getAttribute
&& li
.getAttribute('data-value') === item
)
939 tpl
= sb
.querySelector(sbox
.options
.create_template
);
942 markup
= (tpl
.textContent
|| tpl
.innerHTML
|| tpl
.firstChild
.data
).replace(/^<!--|-->$/, '').trim();
944 markup
= '<li data-value="{{value}}">{{value}}</li>';
946 new_item
= E(markup
.replace(/{{value}}/g, '%h'.format(item
)));
948 if (sbox
.options
.multiple
) {
949 sbox
.transformItem(sb
, new_item
);
952 var old
= ul
.querySelector('li[created]');
956 new_item
.setAttribute('created', '');
959 new_item
= ul
.insertBefore(new_item
, ul
.lastElementChild
);
962 sbox
.toggleItem(sb
, new_item
, true);
963 sbox
.setFocus(sb
, new_item
, true);
967 closeAllDropdowns: function() {
968 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
969 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
973 handleClick: function(ev
) {
974 var sb
= ev
.currentTarget
;
976 if (!sb
.hasAttribute('open')) {
977 if (!matchesElem(ev
.target
, 'input'))
978 this.openDropdown(sb
);
981 var li
= findParent(ev
.target
, 'li');
982 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
983 this.toggleItem(sb
, li
);
984 else if (li
&& li
.parentNode
.classList
.contains('preview'))
985 this.closeDropdown(sb
);
986 else if (matchesElem(ev
.target
, 'span.open, span.more'))
987 this.closeDropdown(sb
);
991 ev
.stopPropagation();
994 handleKeydown: function(ev
) {
995 var sb
= ev
.currentTarget
;
997 if (matchesElem(ev
.target
, 'input'))
1000 if (!sb
.hasAttribute('open')) {
1001 switch (ev
.keyCode
) {
1006 this.openDropdown(sb
);
1007 ev
.preventDefault();
1011 var active
= findParent(document
.activeElement
, 'li');
1013 switch (ev
.keyCode
) {
1015 this.closeDropdown(sb
);
1020 if (!active
.hasAttribute('selected'))
1021 this.toggleItem(sb
, active
);
1022 this.closeDropdown(sb
);
1023 ev
.preventDefault();
1029 this.toggleItem(sb
, active
);
1030 ev
.preventDefault();
1035 if (active
&& active
.previousElementSibling
) {
1036 this.setFocus(sb
, active
.previousElementSibling
);
1037 ev
.preventDefault();
1042 if (active
&& active
.nextElementSibling
) {
1043 this.setFocus(sb
, active
.nextElementSibling
);
1044 ev
.preventDefault();
1051 handleDropdownClose: function(ev
) {
1052 var sb
= ev
.currentTarget
;
1054 this.closeDropdown(sb
, true);
1057 handleDropdownSelect: function(ev
) {
1058 var sb
= ev
.currentTarget
,
1059 li
= findParent(ev
.target
, 'li');
1064 this.toggleItem(sb
, li
);
1065 this.closeDropdown(sb
, true);
1068 handleMouseover: function(ev
) {
1069 var sb
= ev
.currentTarget
;
1071 if (!sb
.hasAttribute('open'))
1074 var li
= findParent(ev
.target
, 'li');
1076 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1077 this.setFocus(sb
, li
);
1080 handleFocus: function(ev
) {
1081 var sb
= ev
.currentTarget
;
1083 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1084 if (s
!== sb
|| sb
.hasAttribute('open'))
1085 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1089 handleCanaryFocus: function(ev
) {
1090 this.closeDropdown(ev
.currentTarget
.parentNode
);
1093 handleCreateKeydown: function(ev
) {
1094 var input
= ev
.currentTarget
,
1095 sb
= findParent(input
, '.cbi-dropdown');
1097 switch (ev
.keyCode
) {
1099 ev
.preventDefault();
1101 if (input
.classList
.contains('cbi-input-invalid'))
1104 this.createItems(sb
, input
.value
);
1111 handleCreateFocus: function(ev
) {
1112 var input
= ev
.currentTarget
,
1113 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1114 sb
= findParent(input
, '.cbi-dropdown');
1117 cbox
.checked
= true;
1119 sb
.setAttribute('locked-in', '');
1122 handleCreateBlur: function(ev
) {
1123 var input
= ev
.currentTarget
,
1124 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1125 sb
= findParent(input
, '.cbi-dropdown');
1128 cbox
.checked
= false;
1130 sb
.removeAttribute('locked-in');
1133 handleCreateClick: function(ev
) {
1134 ev
.currentTarget
.querySelector(this.options
.create_query
).focus();
1137 setValue: function(values
) {
1138 if (this.options
.multiple
) {
1139 if (!Array
.isArray(values
))
1140 values
= (values
!= null && values
!= '') ? [ values
] : [];
1144 for (var i
= 0; i
< values
.length
; i
++)
1145 v
[values
[i
]] = true;
1147 this.setValues(this.node
, v
);
1152 if (values
!= null) {
1153 if (Array
.isArray(values
))
1154 v
[values
[0]] = true;
1159 this.setValues(this.node
, v
);
1163 getValue: function() {
1164 var div
= this.node
.lastElementChild
,
1165 h
= div
.querySelectorAll('input[type="hidden"]'),
1168 for (var i
= 0; i
< h
.length
; i
++)
1171 return this.options
.multiple
? v
: v
[0];
1175 var UICombobox
= UIDropdown
.extend({
1176 __init__: function(value
, choices
, options
) {
1177 this.super('__init__', [ value
, choices
, Object
.assign({
1178 select_placeholder
: _('-- Please choose --'),
1179 custom_placeholder
: _('-- custom --'),
1190 var UIDynamicList
= UIElement
.extend({
1191 __init__: function(values
, choices
, options
) {
1192 if (!Array
.isArray(values
))
1193 values
= (values
!= null && values
!= '') ? [ values
] : [];
1195 if (typeof(choices
) != 'object')
1198 this.values
= values
;
1199 this.choices
= choices
;
1200 this.options
= Object
.assign({}, options
, {
1206 render: function() {
1208 'id': this.options
.id
,
1209 'class': 'cbi-dynlist'
1210 }, E('div', { 'class': 'add-item' }));
1213 var cbox
= new UICombobox(null, this.choices
, this.options
);
1214 dl
.lastElementChild
.appendChild(cbox
.render());
1217 var inputEl
= E('input', {
1218 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1220 'class': 'cbi-input-text',
1221 'placeholder': this.options
.placeholder
1224 dl
.lastElementChild
.appendChild(inputEl
);
1225 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1227 if (this.options
.datatype
)
1228 L
.ui
.addValidator(inputEl
, this.options
.datatype
,
1229 true, null, 'blur', 'keyup');
1232 for (var i
= 0; i
< this.values
.length
; i
++)
1233 this.addItem(dl
, this.values
[i
],
1234 this.choices
? this.choices
[this.values
[i
]] : null);
1236 return this.bind(dl
);
1239 bind: function(dl
) {
1240 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1241 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1242 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1246 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1247 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1249 L
.dom
.bindClassInstance(dl
, this);
1254 addItem: function(dl
, value
, text
, flash
) {
1256 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1257 E('span', {}, text
|| value
),
1260 'name': this.options
.name
,
1261 'value': value
})]);
1263 dl
.querySelectorAll('.item').forEach(function(item
) {
1267 var hidden
= item
.querySelector('input[type="hidden"]');
1269 if (hidden
&& hidden
.parentNode
!== item
)
1272 if (hidden
&& hidden
.value
=== value
)
1277 var ai
= dl
.querySelector('.add-item');
1278 ai
.parentNode
.insertBefore(new_item
, ai
);
1281 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1292 removeItem: function(dl
, item
) {
1293 var value
= item
.querySelector('input[type="hidden"]').value
;
1294 var sb
= dl
.querySelector('.cbi-dropdown');
1296 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1297 if (li
.getAttribute('data-value') === value
) {
1298 if (li
.hasAttribute('dynlistcustom'))
1299 li
.parentNode
.removeChild(li
);
1301 li
.removeAttribute('unselectable');
1305 item
.parentNode
.removeChild(item
);
1307 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1318 handleClick: function(ev
) {
1319 var dl
= ev
.currentTarget
,
1320 item
= findParent(ev
.target
, '.item');
1323 this.removeItem(dl
, item
);
1325 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1326 var input
= ev
.target
.previousElementSibling
;
1327 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1328 this.addItem(dl
, input
.value
, null, true);
1334 handleDropdownChange: function(ev
) {
1335 var dl
= ev
.currentTarget
,
1336 sbIn
= ev
.detail
.instance
,
1337 sbEl
= ev
.detail
.element
,
1338 sbVal
= ev
.detail
.value
;
1343 sbIn
.setValues(sbEl
, null);
1344 sbVal
.element
.setAttribute('unselectable', '');
1346 if (sbVal
.element
.hasAttribute('created')) {
1347 sbVal
.element
.removeAttribute('created');
1348 sbVal
.element
.setAttribute('dynlistcustom', '');
1351 this.addItem(dl
, sbVal
.value
, sbVal
.text
, true);
1354 handleKeydown: function(ev
) {
1355 var dl
= ev
.currentTarget
,
1356 item
= findParent(ev
.target
, '.item');
1359 switch (ev
.keyCode
) {
1360 case 8: /* backspace */
1361 if (item
.previousElementSibling
)
1362 item
.previousElementSibling
.focus();
1364 this.removeItem(dl
, item
);
1367 case 46: /* delete */
1368 if (item
.nextElementSibling
) {
1369 if (item
.nextElementSibling
.classList
.contains('item'))
1370 item
.nextElementSibling
.focus();
1372 item
.nextElementSibling
.firstElementChild
.focus();
1375 this.removeItem(dl
, item
);
1379 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1380 switch (ev
.keyCode
) {
1381 case 13: /* enter */
1382 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1383 this.addItem(dl
, ev
.target
.value
, null, true);
1384 ev
.target
.value
= '';
1389 ev
.preventDefault();
1395 getValue: function() {
1396 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1397 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1400 for (var i
= 0; i
< items
.length
; i
++)
1401 v
.push(items
[i
].value
);
1403 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1404 input
.classList
.contains('cbi-input-invalid') == false &&
1405 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1406 v
.push(input
.value
);
1411 setValue: function(values
) {
1412 if (!Array
.isArray(values
))
1413 values
= (values
!= null && values
!= '') ? [ values
] : [];
1415 var items
= this.node
.querySelectorAll('.item');
1417 for (var i
= 0; i
< items
.length
; i
++)
1418 if (items
[i
].parentNode
=== this.node
)
1419 this.removeItem(this.node
, items
[i
]);
1421 for (var i
= 0; i
< values
.length
; i
++)
1422 this.addItem(this.node
, values
[i
],
1423 this.choices
? this.choices
[values
[i
]] : null);
1427 var UIHiddenfield
= UIElement
.extend({
1428 __init__: function(value
, options
) {
1430 this.options
= Object
.assign({
1435 render: function() {
1436 var hiddenEl
= E('input', {
1437 'id': this.options
.id
,
1442 return this.bind(hiddenEl
);
1445 bind: function(hiddenEl
) {
1446 this.node
= hiddenEl
;
1448 L
.dom
.bindClassInstance(hiddenEl
, this);
1453 getValue: function() {
1454 return this.node
.value
;
1457 setValue: function(value
) {
1458 this.node
.value
= value
;
1462 var UIFileUpload
= UIElement
.extend({
1463 __init__: function(value
, options
) {
1465 this.options
= Object
.assign({
1467 enable_upload
: true,
1468 enable_remove
: true,
1469 root_directory
: '/etc/luci-uploads'
1473 callFileStat
: rpc
.declare({
1476 'params': [ 'path' ],
1477 'expect': { '': {} }
1480 callFileList
: rpc
.declare({
1483 'params': [ 'path' ],
1484 'expect': { 'entries': [] }
1487 callFileRemove
: rpc
.declare({
1490 'params': [ 'path' ]
1493 bind: function(browserEl
) {
1494 this.node
= browserEl
;
1496 this.setUpdateEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1497 this.setChangeEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1499 L
.dom
.bindClassInstance(browserEl
, this);
1504 render: function() {
1505 return Promise
.resolve(this.value
!= null ? this.callFileStat(this.value
) : null).then(L
.bind(function(stat
) {
1508 if (L
.isObject(stat
) && stat
.type
!= 'directory')
1511 if (this.stat
!= null)
1512 label
= [ this.iconForType(this.stat
.type
), ' %s (%1000mB)'.format(this.truncatePath(this.stat
.path
), this.stat
.size
) ];
1513 else if (this.value
!= null)
1514 label
= [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value
), _('File not accessible')) ];
1516 label
= [ _('Select file…') ];
1518 return this.bind(E('div', { 'id': this.options
.id
}, [
1521 'click': L
.ui
.createHandlerFn(this, 'handleFileBrowser')
1524 'class': 'cbi-filebrowser'
1528 'name': this.options
.name
,
1535 truncatePath: function(path
) {
1536 if (path
.length
> 50)
1537 path
= path
.substring(0, 25) + '…' + path
.substring(path
.length
- 25);
1542 iconForType: function(type
) {
1546 'src': L
.resource('cbi/link.gif'),
1547 'title': _('Symbolic link'),
1553 'src': L
.resource('cbi/folder.gif'),
1554 'title': _('Directory'),
1560 'src': L
.resource('cbi/file.gif'),
1567 canonicalizePath: function(path
) {
1568 return path
.replace(/\/{2,}/, '/')
1569 .replace(/\/\.(\/|$)/g, '/')
1570 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1571 .replace(/\/$/, '');
1574 splitPath: function(path
) {
1575 var croot
= this.canonicalizePath(this.options
.root_directory
|| '/'),
1576 cpath
= this.canonicalizePath(path
|| '/');
1578 if (cpath
.length
<= croot
.length
)
1581 if (cpath
.charAt(croot
.length
) != '/')
1584 var parts
= cpath
.substring(croot
.length
+ 1).split(/\//);
1586 parts
.unshift(croot
);
1591 handleUpload: function(path
, list
, ev
) {
1592 var form
= ev
.target
.parentNode
,
1593 fileinput
= form
.querySelector('input[type="file"]'),
1594 nameinput
= form
.querySelector('input[type="text"]'),
1595 filename
= (nameinput
.value
!= null ? nameinput
.value
: '').trim();
1597 ev
.preventDefault();
1599 if (filename
== '' || filename
.match(/\//) || fileinput
.files
[0] == null)
1602 var existing
= list
.filter(function(e
) { return e
.name
== filename
})[0];
1604 if (existing
!= null && existing
.type
== 'directory')
1605 return alert(_('A directory with the same name already exists.'));
1606 else if (existing
!= null && !confirm(_('Overwrite existing file "%s" ?').format(filename
)))
1609 var data
= new FormData();
1611 data
.append('sessionid', L
.env
.sessionid
);
1612 data
.append('filename', path
+ '/' + filename
);
1613 data
.append('filedata', fileinput
.files
[0]);
1615 return L
.Request
.post('/cgi-bin/cgi-upload', data
, {
1616 progress
: L
.bind(function(btn
, ev
) {
1617 btn
.firstChild
.data
= '%.2f%%'.format((ev
.loaded
/ ev
.total
) * 100);
1619 }).then(L
.bind(function(path
, ev
, res
) {
1620 var reply
= res
.json();
1622 if (L
.isObject(reply
) && reply
.failure
)
1623 alert(_('Upload request failed: %s').format(reply
.message
));
1625 return this.handleSelect(path
, null, ev
);
1626 }, this, path
, ev
));
1629 handleDelete: function(path
, fileStat
, ev
) {
1630 var parent
= path
.replace(/\/[^\/]+$/, '') || '/',
1631 name
= path
.replace(/^.+\//, ''),
1634 ev
.preventDefault();
1636 if (fileStat
.type
== 'directory')
1637 msg
= _('Do you really want to recursively delete the directory "%s" ?').format(name
);
1639 msg
= _('Do you really want to delete "%s" ?').format(name
);
1642 var button
= this.node
.firstElementChild
,
1643 hidden
= this.node
.lastElementChild
;
1645 if (path
== hidden
.value
) {
1646 L
.dom
.content(button
, _('Select file…'));
1650 return this.callFileRemove(path
).then(L
.bind(function(parent
, ev
, rc
) {
1652 return this.handleSelect(parent
, null, ev
);
1654 alert(_('Delete permission denied'));
1656 alert(_('Delete request failed: %d %s').format(rc
, rpc
.getStatusText(rc
)));
1658 }, this, parent
, ev
));
1662 renderUpload: function(path
, list
) {
1663 if (!this.options
.enable_upload
)
1669 'class': 'btn cbi-button-positive',
1670 'click': function(ev
) {
1671 var uploadForm
= ev
.target
.nextElementSibling
,
1672 fileInput
= uploadForm
.querySelector('input[type="file"]');
1674 ev
.target
.style
.display
= 'none';
1675 uploadForm
.style
.display
= '';
1678 }, _('Upload file…')),
1679 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1682 'style': 'display:none',
1683 'change': function(ev
) {
1684 var nameinput
= ev
.target
.parentNode
.querySelector('input[type="text"]'),
1685 uploadbtn
= ev
.target
.parentNode
.querySelector('button.cbi-button-save');
1687 nameinput
.value
= ev
.target
.value
.replace(/^.+[\/\\]/, '');
1688 uploadbtn
.disabled
= false;
1693 'click': function(ev
) {
1694 ev
.preventDefault();
1695 ev
.target
.previousElementSibling
.click();
1697 }, [ _('Browse…') ]),
1698 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1700 'class': 'btn cbi-button-save',
1701 'click': L
.ui
.createHandlerFn(this, 'handleUpload', path
, list
),
1703 }, [ _('Upload file') ])
1708 renderListing: function(container
, path
, list
) {
1709 var breadcrumb
= E('p'),
1712 list
.sort(function(a
, b
) {
1713 var isDirA
= (a
.type
== 'directory'),
1714 isDirB
= (b
.type
== 'directory');
1716 if (isDirA
!= isDirB
)
1717 return isDirA
< isDirB
;
1719 return a
.name
> b
.name
;
1722 for (var i
= 0; i
< list
.length
; i
++) {
1723 if (!this.options
.show_hidden
&& list
[i
].name
.charAt(0) == '.')
1726 var entrypath
= this.canonicalizePath(path
+ '/' + list
[i
].name
),
1727 selected
= (entrypath
== this.node
.lastElementChild
.value
),
1728 mtime
= new Date(list
[i
].mtime
* 1000);
1730 rows
.appendChild(E('li', [
1731 E('div', { 'class': 'name' }, [
1732 this.iconForType(list
[i
].type
),
1736 'style': selected
? 'font-weight:bold' : null,
1737 'click': L
.ui
.createHandlerFn(this, 'handleSelect',
1738 entrypath
, list
[i
].type
!= 'directory' ? list
[i
] : null)
1739 }, '%h'.format(list
[i
].name
))
1741 E('div', { 'class': 'mtime hide-xs' }, [
1742 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1743 mtime
.getFullYear(),
1744 mtime
.getMonth() + 1,
1751 selected
? E('button', {
1753 'click': L
.ui
.createHandlerFn(this, 'handleReset')
1754 }, [ _('Deselect') ]) : '',
1755 this.options
.enable_remove
? E('button', {
1756 'class': 'btn cbi-button-negative',
1757 'click': L
.ui
.createHandlerFn(this, 'handleDelete', entrypath
, list
[i
])
1758 }, [ _('Delete') ]) : ''
1763 if (!rows
.firstElementChild
)
1764 rows
.appendChild(E('em', _('No entries in this directory')));
1766 var dirs
= this.splitPath(path
),
1769 for (var i
= 0; i
< dirs
.length
; i
++) {
1770 cur
= cur
? cur
+ '/' + dirs
[i
] : dirs
[i
];
1771 L
.dom
.append(breadcrumb
, [
1775 'click': L
.ui
.createHandlerFn(this, 'handleSelect', cur
|| '/', null)
1776 }, dirs
[i
] != '' ? '%h'.format(dirs
[i
]) : E('em', '(root)')),
1780 L
.dom
.content(container
, [
1783 E('div', { 'class': 'right' }, [
1784 this.renderUpload(path
, list
),
1788 'click': L
.ui
.createHandlerFn(this, 'handleCancel')
1794 handleCancel: function(ev
) {
1795 var button
= this.node
.firstElementChild
,
1796 browser
= button
.nextElementSibling
;
1798 browser
.classList
.remove('open');
1799 button
.style
.display
= '';
1801 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1804 handleReset: function(ev
) {
1805 var button
= this.node
.firstElementChild
,
1806 hidden
= this.node
.lastElementChild
;
1809 L
.dom
.content(button
, _('Select file…'));
1811 this.handleCancel(ev
);
1814 handleSelect: function(path
, fileStat
, ev
) {
1815 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1816 ul
= browser
.querySelector('ul');
1818 if (fileStat
== null) {
1819 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1820 this.callFileList(path
).then(L
.bind(this.renderListing
, this, browser
, path
));
1823 var button
= this.node
.firstElementChild
,
1824 hidden
= this.node
.lastElementChild
;
1826 path
= this.canonicalizePath(path
);
1828 L
.dom
.content(button
, [
1829 this.iconForType(fileStat
.type
),
1830 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1833 browser
.classList
.remove('open');
1834 button
.style
.display
= '';
1835 hidden
.value
= path
;
1837 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1838 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1842 handleFileBrowser: function(ev
) {
1843 var button
= ev
.target
,
1844 browser
= button
.nextElementSibling
,
1845 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : this.options
.root_directory
;
1847 if (this.options
.root_directory
.indexOf(path
) != 0)
1848 path
= this.options
.root_directory
;
1850 ev
.preventDefault();
1852 return this.callFileList(path
).then(L
.bind(function(button
, browser
, path
, list
) {
1853 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1854 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1857 button
.style
.display
= 'none';
1858 browser
.classList
.add('open');
1860 return this.renderListing(browser
, path
, list
);
1861 }, this, button
, browser
, path
));
1864 getValue: function() {
1865 return this.node
.lastElementChild
.value
;
1868 setValue: function(value
) {
1869 this.node
.lastElementChild
.value
= value
;
1874 return L
.Class
.extend({
1875 __init__: function() {
1876 modalDiv
= document
.body
.appendChild(
1877 L
.dom
.create('div', { id
: 'modal_overlay' },
1878 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1880 tooltipDiv
= document
.body
.appendChild(
1881 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1883 /* setup old aliases */
1884 L
.showModal
= this.showModal
;
1885 L
.hideModal
= this.hideModal
;
1886 L
.showTooltip
= this.showTooltip
;
1887 L
.hideTooltip
= this.hideTooltip
;
1888 L
.itemlist
= this.itemlist
;
1890 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1891 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1892 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1893 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1895 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1896 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1897 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1901 showModal: function(title
, children
/* , ... */) {
1902 var dlg
= modalDiv
.firstElementChild
;
1904 dlg
.setAttribute('class', 'modal');
1906 for (var i
= 2; i
< arguments
.length
; i
++)
1907 dlg
.classList
.add(arguments
[i
]);
1909 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1910 L
.dom
.append(dlg
, children
);
1912 document
.body
.classList
.add('modal-overlay-active');
1917 hideModal: function() {
1918 document
.body
.classList
.remove('modal-overlay-active');
1922 showTooltip: function(ev
) {
1923 var target
= findParent(ev
.target
, '[data-tooltip]');
1928 if (tooltipTimeout
!== null) {
1929 window
.clearTimeout(tooltipTimeout
);
1930 tooltipTimeout
= null;
1933 var rect
= target
.getBoundingClientRect(),
1934 x
= rect
.left
+ window
.pageXOffset
,
1935 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
1937 tooltipDiv
.className
= 'cbi-tooltip';
1938 tooltipDiv
.innerHTML
= '▲ ';
1939 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
1941 if (target
.hasAttribute('data-tooltip-style'))
1942 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
1944 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
1945 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
1946 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
1949 tooltipDiv
.style
.top
= y
+ 'px';
1950 tooltipDiv
.style
.left
= x
+ 'px';
1951 tooltipDiv
.style
.opacity
= 1;
1953 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
1955 detail
: { target
: target
}
1959 hideTooltip: function(ev
) {
1960 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
1961 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
1964 if (tooltipTimeout
!== null) {
1965 window
.clearTimeout(tooltipTimeout
);
1966 tooltipTimeout
= null;
1969 tooltipDiv
.style
.opacity
= 0;
1970 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
1972 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
1975 addNotification: function(title
, children
/*, ... */) {
1976 var mc
= document
.querySelector('#maincontent') || document
.body
;
1977 var msg
= E('div', {
1978 'class': 'alert-message fade-in',
1979 'style': 'display:flex',
1980 'transitionend': function(ev
) {
1981 var node
= ev
.currentTarget
;
1982 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
1983 node
.parentNode
.removeChild(node
);
1986 E('div', { 'style': 'flex:10' }),
1987 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
1990 'style': 'margin-left:auto; margin-top:auto',
1991 'click': function(ev
) {
1992 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
1995 }, [ _('Dismiss') ])
2000 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
2002 L
.dom
.append(msg
.firstElementChild
, children
);
2004 for (var i
= 2; i
< arguments
.length
; i
++)
2005 msg
.classList
.add(arguments
[i
]);
2007 mc
.insertBefore(msg
, mc
.firstElementChild
);
2013 itemlist: function(node
, items
, separators
) {
2016 if (!Array
.isArray(separators
))
2017 separators
= [ separators
|| E('br') ];
2019 for (var i
= 0; i
< items
.length
; i
+= 2) {
2020 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
2021 var sep
= separators
[(i
/2) % separators
.length
],
2024 children
.push(E('span', { class: 'nowrap' }, [
2025 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2029 if ((i
+2) < items
.length
)
2030 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2034 L
.dom
.content(node
, children
);
2040 tabs
: L
.Class
.singleton({
2042 var groups
= [], prevGroup
= null, currGroup
= null;
2044 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2045 var parent
= tab
.parentNode
;
2047 if (!parent
.hasAttribute('data-tab-group'))
2048 parent
.setAttribute('data-tab-group', groups
.length
);
2050 currGroup
= +parent
.getAttribute('data-tab-group');
2052 if (currGroup
!== prevGroup
) {
2053 prevGroup
= currGroup
;
2055 if (!groups
[currGroup
])
2056 groups
[currGroup
] = [];
2059 groups
[currGroup
].push(tab
);
2062 for (var i
= 0; i
< groups
.length
; i
++)
2063 this.initTabGroup(groups
[i
]);
2065 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2070 initTabGroup: function(panes
) {
2071 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2074 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2075 group
= panes
[0].parentNode
,
2076 groupId
= +group
.getAttribute('data-tab-group'),
2079 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2080 var name
= pane
.getAttribute('data-tab'),
2081 title
= pane
.getAttribute('data-tab-title'),
2082 active
= pane
.getAttribute('data-tab-active') === 'true';
2084 menu
.appendChild(E('li', {
2085 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2086 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2090 'click': this.switchTab
.bind(this)
2097 group
.parentNode
.insertBefore(menu
, group
);
2099 if (selected
=== null) {
2100 selected
= this.getActiveTabId(panes
[0]);
2102 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2103 for (var i
= 0; i
< panes
.length
; i
++) {
2104 if (!this.isEmptyPane(panes
[i
])) {
2111 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2112 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2113 panes
[selected
].setAttribute('data-tab-active', 'true');
2115 this.setActiveTabId(panes
[selected
], selected
);
2118 this.updateTabs(group
);
2121 isEmptyPane: function(pane
) {
2122 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2125 getPathForPane: function(pane
) {
2126 var path
= [], node
= null;
2128 for (node
= pane
? pane
.parentNode
: null;
2129 node
!= null && node
.hasAttribute
!= null;
2130 node
= node
.parentNode
)
2132 if (node
.hasAttribute('data-tab'))
2133 path
.unshift(node
.getAttribute('data-tab'));
2134 else if (node
.hasAttribute('data-section-id'))
2135 path
.unshift(node
.getAttribute('data-section-id'));
2138 return path
.join('/');
2141 getActiveTabState: function() {
2142 var page
= document
.body
.getAttribute('data-page');
2145 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2146 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2151 window
.sessionStorage
.removeItem('tab');
2152 return { page
: page
, paths
: {} };
2155 getActiveTabId: function(pane
) {
2156 var path
= this.getPathForPane(pane
);
2157 return +this.getActiveTabState().paths
[path
] || 0;
2160 setActiveTabId: function(pane
, tabIndex
) {
2161 var path
= this.getPathForPane(pane
);
2164 var state
= this.getActiveTabState();
2165 state
.paths
[path
] = tabIndex
;
2167 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2169 catch (e
) { return false; }
2174 updateTabs: function(ev
, root
) {
2175 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2176 var menu
= pane
.parentNode
.previousElementSibling
,
2177 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2178 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2183 if (this.isEmptyPane(pane
)) {
2184 tab
.style
.display
= 'none';
2185 tab
.classList
.remove('flash');
2187 else if (tab
.style
.display
=== 'none') {
2188 tab
.style
.display
= '';
2189 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2193 tab
.setAttribute('data-errors', n_errors
);
2194 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2195 tab
.setAttribute('data-tooltip-style', 'error');
2198 tab
.removeAttribute('data-errors');
2199 tab
.removeAttribute('data-tooltip');
2204 switchTab: function(ev
) {
2205 var tab
= ev
.target
.parentNode
,
2206 name
= tab
.getAttribute('data-tab'),
2207 menu
= tab
.parentNode
,
2208 group
= menu
.nextElementSibling
,
2209 groupId
= +group
.getAttribute('data-tab-group'),
2212 ev
.preventDefault();
2214 if (!tab
.classList
.contains('cbi-tab-disabled'))
2217 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2218 tab
.classList
.remove('cbi-tab');
2219 tab
.classList
.remove('cbi-tab-disabled');
2221 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2224 group
.childNodes
.forEach(function(pane
) {
2225 if (L
.dom
.matches(pane
, '[data-tab]')) {
2226 if (pane
.getAttribute('data-tab') === name
) {
2227 pane
.setAttribute('data-tab-active', 'true');
2228 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2231 pane
.setAttribute('data-tab-active', 'false');
2241 changes
: L
.Class
.singleton({
2243 if (!L
.env
.sessionid
)
2246 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2249 setIndicator: function(n
) {
2250 var i
= document
.querySelector('.uci_change_indicator');
2252 var poll
= document
.getElementById('xhr_poll_status');
2253 i
= poll
.parentNode
.insertBefore(E('a', {
2255 'class': 'uci_change_indicator label notice',
2256 'click': L
.bind(this.displayChanges
, this)
2261 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2262 i
.classList
.add('flash');
2263 i
.style
.display
= '';
2266 i
.classList
.remove('flash');
2267 i
.style
.display
= 'none';
2271 renderChangeIndicator: function(changes
) {
2274 for (var config
in changes
)
2275 if (changes
.hasOwnProperty(config
))
2276 n_changes
+= changes
[config
].length
;
2278 this.changes
= changes
;
2279 this.setIndicator(n_changes
);
2283 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2284 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2285 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2286 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2287 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2288 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2289 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2290 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2291 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2292 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2295 displayChanges: function() {
2296 var list
= E('div', { 'class': 'uci-change-list' }),
2297 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2298 E('div', { 'class': 'cbi-section' }, [
2299 E('strong', _('Legend:')),
2300 E('div', { 'class': 'uci-change-legend' }, [
2301 E('div', { 'class': 'uci-change-legend-label' }, [
2302 E('ins', ' '), ' ', _('Section added') ]),
2303 E('div', { 'class': 'uci-change-legend-label' }, [
2304 E('del', ' '), ' ', _('Section removed') ]),
2305 E('div', { 'class': 'uci-change-legend-label' }, [
2306 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2307 E('div', { 'class': 'uci-change-legend-label' }, [
2308 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2310 E('div', { 'class': 'right' }, [
2313 'click': L
.ui
.hideModal
2314 }, [ _('Dismiss') ]), ' ',
2316 'class': 'cbi-button cbi-button-positive important',
2317 'click': L
.bind(this.apply
, this, true)
2318 }, [ _('Save & Apply') ]), ' ',
2320 'class': 'cbi-button cbi-button-reset',
2321 'click': L
.bind(this.revert
, this)
2322 }, [ _('Revert') ])])])
2325 for (var config
in this.changes
) {
2326 if (!this.changes
.hasOwnProperty(config
))
2329 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2331 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2332 var chg
= this.changes
[config
][i
],
2333 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2335 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2341 if (added
!= null && chg
[1] == added
[0])
2342 return '@' + added
[1] + '[-1]';
2347 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2354 if (chg[0] == 'add')
2355 added = [ chg[1], chg[2] ];
2359 list.appendChild(E('br'));
2360 dlg.classList.add('uci-dialog');
2363 displayStatus: function(type, content) {
2365 var message = L.ui.showModal('', '');
2367 message.classList.add('alert-message');
2368 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2371 L.dom.content(message, content);
2373 if (!this.was_polling) {
2374 this.was_polling = L.Request.poll.active();
2375 L.Request.poll.stop();
2381 if (this.was_polling)
2382 L.Request.poll.start();
2386 rollback: function(checked) {
2388 this.displayStatus('warning spinning',
2389 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2390 .format(L.env.apply_rollback)));
2392 var call = function(r, data, duration) {
2393 if (r.status === 204) {
2394 L.ui.changes.displayStatus('warning', [
2395 E('h4', _('Configuration has been rolled back!')),
2396 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)),
2397 E('div', { 'class': 'right' }, [
2400 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2401 }, [ _('Dismiss') ]), ' ',
2403 'class': 'btn cbi-button-action important',
2404 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2405 }, [ _('Revert changes') ]), ' ',
2407 'class': 'btn cbi-button-negative important',
2408 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2409 }, [ _('Apply unchecked') ])
2416 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2417 window.setTimeout(function() {
2418 L.Request.request(L.url('admin/uci/confirm'), {
2420 timeout: L.env.apply_timeout * 1000,
2421 query: { sid: L.env.sessionid, token: L.env.token }
2426 call({ status: 0 });
2429 this.displayStatus('warning', [
2430 E('h4', _('Device unreachable!')),
2431 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.'))
2436 confirm: function(checked, deadline, override_token) {
2438 var ts = Date.now();
2440 this.displayStatus('notice');
2443 this.confirm_auth = { token: override_token };
2445 var call = function(r, data, duration) {
2446 if (Date.now() >= deadline) {
2447 window.clearTimeout(tt);
2448 L.ui.changes.rollback(checked);
2451 else if (r && (r.status === 200 || r.status === 204)) {
2452 document.dispatchEvent(new CustomEvent('uci-applied'));
2454 L.ui.changes.setIndicator(0);
2455 L.ui.changes.displayStatus('notice',
2456 E('p', _('Configuration has been applied.')));
2458 window.clearTimeout(tt);
2459 window.setTimeout(function() {
2460 //L.ui.changes.displayStatus(false);
2461 window.location = window.location.href.split('#')[0];
2462 }, L.env.apply_display * 1000);
2467 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2468 window.setTimeout(function() {
2469 L.Request.request(L.url('admin/uci/confirm'), {
2471 timeout: L.env.apply_timeout * 1000,
2472 query: L.ui.changes.confirm_auth
2473 }).then(call, call);
2477 var tick = function() {
2478 var now = Date.now();
2480 L.ui.changes.displayStatus('notice spinning',
2481 E('p', _('Waiting for configuration to get applied… %ds')
2482 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2484 if (now >= deadline)
2487 tt = window.setTimeout(tick, 1000 - (now - ts));
2493 /* wait a few seconds for the settings to become effective */
2494 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2497 apply: function(checked) {
2498 this.displayStatus('notice spinning',
2499 E('p', _('Starting configuration apply…')));
2501 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2503 query: { sid: L.env.sessionid, token: L.env.token }
2504 }).then(function(r) {
2505 if (r.status === (checked ? 200 : 204)) {
2506 var tok = null; try { tok = r.json(); } catch(e) {}
2507 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2508 L.ui.changes.confirm_auth = tok;
2510 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2512 else if (checked && r.status === 204) {
2513 L.ui.changes.displayStatus('notice',
2514 E('p', _('There are no changes to apply')));
2516 window.setTimeout(function() {
2517 L.ui.changes.displayStatus(false);
2518 }, L.env.apply_display * 1000);
2521 L.ui.changes.displayStatus('warning',
2522 E('p', _('Apply request failed with status <code>%h</code>')
2523 .format(r.responseText || r.statusText || r.status)));
2525 window.setTimeout(function() {
2526 L.ui.changes.displayStatus(false);
2527 }, L.env.apply_display * 1000);
2532 revert: function() {
2533 this.displayStatus('notice spinning',
2534 E('p', _('Reverting configuration…')));
2536 L.Request.request(L.url('admin/uci/revert'), {
2538 query: { sid: L.env.sessionid, token: L.env.token }
2539 }).then(function(r) {
2540 if (r.status === 200) {
2541 document.dispatchEvent(new CustomEvent('uci-reverted'));
2543 L.ui.changes.setIndicator(0);
2544 L.ui.changes.displayStatus('notice',
2545 E('p', _('Changes have been reverted.')));
2547 window.setTimeout(function() {
2548 //L.ui.changes.displayStatus(false);
2549 window.location = window.location.href.split('#')[0];
2550 }, L.env.apply_display * 1000);
2553 L.ui.changes.displayStatus('warning',
2554 E('p', _('Revert request failed with status <code>%h</code>')
2555 .format(r.statusText || r.status)));
2557 window.setTimeout(function() {
2558 L.ui.changes.displayStatus(false);
2559 }, L.env.apply_display * 1000);
2565 addValidator: function(field, type, optional, vfunc /*, ... */) {
2569 var events = this.varargs(arguments, 3);
2570 if (events.length == 0)
2571 events.push('blur', 'keyup');
2574 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2575 validatorFn = cbiValidator.validate.bind(cbiValidator);
2577 for (var i = 0; i < events.length; i++)
2578 field.addEventListener(events[i], validatorFn);
2587 createHandlerFn: function(ctx, fn /*, ... */) {
2588 if (typeof(fn) == 'string')
2591 if (typeof(fn) != 'function')
2594 return Function.prototype.bind.apply(function() {
2595 var t = arguments[arguments.length - 1].target;
2597 t.classList.add('spinning');
2603 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2604 t.classList.remove('spinning');
2607 }, this.varargs(arguments, 2, ctx));
2611 Textfield: UITextfield,
2612 Textarea: UITextarea,
2613 Checkbox: UICheckbox,
2615 Dropdown: UIDropdown,
2616 DynamicList: UIDynamicList,
2617 Combobox: UICombobox,
2618 Hiddenfield: UIHiddenfield,
2619 FileUpload: UIFileUpload