11 var UIElement
= L
.Class
.extend({
12 getValue: function() {
13 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
14 return this.node
.value
;
19 setValue: function(value
) {
20 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
21 this.node
.value
= value
;
25 return (this.validState
!== false);
28 triggerValidation: function() {
29 if (typeof(this.vfunc
) != 'function')
32 var wasValid
= this.isValid();
36 return (wasValid
!= this.isValid());
39 registerEvents: function(targetNode
, synevent
, events
) {
40 var dispatchFn
= L
.bind(function(ev
) {
41 this.node
.dispatchEvent(new CustomEvent(synevent
, { bubbles
: true }));
44 for (var i
= 0; i
< events
.length
; i
++)
45 targetNode
.addEventListener(events
[i
], dispatchFn
);
48 setUpdateEvents: function(targetNode
/*, ... */) {
49 var datatype
= this.options
.datatype
,
50 optional
= this.options
.hasOwnProperty('optional') ? this.options
.optional
: true,
51 validate
= this.options
.validate
,
52 events
= this.varargs(arguments
, 1);
54 this.registerEvents(targetNode
, 'widget-update', events
);
56 if (!datatype
&& !validate
)
59 this.vfunc
= L
.ui
.addValidator
.apply(L
.ui
, [
60 targetNode
, datatype
|| 'string',
64 this.node
.addEventListener('validation-success', L
.bind(function(ev
) {
65 this.validState
= true;
68 this.node
.addEventListener('validation-failure', L
.bind(function(ev
) {
69 this.validState
= false;
73 setChangeEvents: function(targetNode
/*, ... */) {
74 var tag_changed
= L
.bind(function(ev
) { this.setAttribute('data-changed', true) }, this.node
);
76 for (var i
= 1; i
< arguments
.length
; i
++)
77 targetNode
.addEventListener(arguments
[i
], tag_changed
);
79 this.registerEvents(targetNode
, 'widget-change', this.varargs(arguments
, 1));
83 var UITextfield
= UIElement
.extend({
84 __init__: function(value
, options
) {
86 this.options
= Object
.assign({
93 var frameEl
= E('div', { 'id': this.options
.id
});
95 if (this.options
.password
) {
96 frameEl
.classList
.add('nowrap');
97 frameEl
.appendChild(E('input', {
99 'style': 'position:absolute; left:-100000px',
102 'name': this.options
.name
? 'password.%s'.format(this.options
.name
) : null
106 frameEl
.appendChild(E('input', {
107 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
108 'name': this.options
.name
,
109 'type': this.options
.password
? 'password' : 'text',
110 'class': this.options
.password
? 'cbi-input-password' : 'cbi-input-text',
111 'readonly': this.options
.readonly
? '' : null,
112 'maxlength': this.options
.maxlength
,
113 'placeholder': this.options
.placeholder
,
117 if (this.options
.password
)
118 frameEl
.appendChild(E('button', {
119 'class': 'cbi-button cbi-button-neutral',
120 'title': _('Reveal/hide password'),
121 'aria-label': _('Reveal/hide password'),
122 'click': function(ev
) {
123 var e
= this.previousElementSibling
;
124 e
.type
= (e
.type
=== 'password') ? 'text' : 'password';
129 return this.bind(frameEl
);
132 bind: function(frameEl
) {
133 var inputEl
= frameEl
.childNodes
[+!!this.options
.password
];
137 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
138 this.setChangeEvents(inputEl
, 'change');
140 L
.dom
.bindClassInstance(frameEl
, this);
145 getValue: function() {
146 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
147 return inputEl
.value
;
150 setValue: function(value
) {
151 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
152 inputEl
.value
= value
;
156 var UITextarea
= UIElement
.extend({
157 __init__: function(value
, options
) {
159 this.options
= Object
.assign({
168 var frameEl
= E('div', { 'id': this.options
.id
}),
169 value
= (this.value
!= null) ? String(this.value
) : '';
171 frameEl
.appendChild(E('textarea', {
172 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
173 'name': this.options
.name
,
174 'class': 'cbi-input-textarea',
175 'readonly': this.options
.readonly
? '' : null,
176 'placeholder': this.options
.placeholder
,
177 'style': !this.options
.cols
? 'width:100%' : null,
178 'cols': this.options
.cols
,
179 'rows': this.options
.rows
,
180 'wrap': this.options
.wrap
? '' : null
183 if (this.options
.monospace
)
184 frameEl
.firstElementChild
.style
.fontFamily
= 'monospace';
186 return this.bind(frameEl
);
189 bind: function(frameEl
) {
190 var inputEl
= frameEl
.firstElementChild
;
194 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
195 this.setChangeEvents(inputEl
, 'change');
197 L
.dom
.bindClassInstance(frameEl
, this);
202 getValue: function() {
203 return this.node
.firstElementChild
.value
;
206 setValue: function(value
) {
207 this.node
.firstElementChild
.value
= value
;
211 var UICheckbox
= UIElement
.extend({
212 __init__: function(value
, options
) {
214 this.options
= Object
.assign({
221 var frameEl
= E('div', {
222 'id': this.options
.id
,
223 'class': 'cbi-checkbox'
226 if (this.options
.hiddenname
)
227 frameEl
.appendChild(E('input', {
229 'name': this.options
.hiddenname
,
233 frameEl
.appendChild(E('input', {
234 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
235 'name': this.options
.name
,
237 'value': this.options
.value_enabled
,
238 'checked': (this.value
== this.options
.value_enabled
) ? '' : null
241 return this.bind(frameEl
);
244 bind: function(frameEl
) {
247 this.setUpdateEvents(frameEl
.lastElementChild
, 'click', 'blur');
248 this.setChangeEvents(frameEl
.lastElementChild
, 'change');
250 L
.dom
.bindClassInstance(frameEl
, this);
255 isChecked: function() {
256 return this.node
.lastElementChild
.checked
;
259 getValue: function() {
260 return this.isChecked()
261 ? this.options
.value_enabled
262 : this.options
.value_disabled
;
265 setValue: function(value
) {
266 this.node
.lastElementChild
.checked
= (value
== this.options
.value_enabled
);
270 var UISelect
= UIElement
.extend({
271 __init__: function(value
, choices
, options
) {
272 if (!L
.isObject(choices
))
275 if (!Array
.isArray(value
))
276 value
= (value
!= null && value
!= '') ? [ value
] : [];
278 if (!options
.multiple
&& value
.length
> 1)
282 this.choices
= choices
;
283 this.options
= Object
.assign({
286 orientation
: 'horizontal'
289 if (this.choices
.hasOwnProperty(''))
290 this.options
.optional
= true;
294 var frameEl
= E('div', { 'id': this.options
.id
}),
295 keys
= Object
.keys(this.choices
);
297 if (this.options
.sort
=== true)
299 else if (Array
.isArray(this.options
.sort
))
300 keys
= this.options
.sort
;
302 if (this.options
.widget
== 'select') {
303 frameEl
.appendChild(E('select', {
304 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
305 'name': this.options
.name
,
306 'size': this.options
.size
,
307 'class': 'cbi-input-select',
308 'multiple': this.options
.multiple
? '' : null
311 if (this.options
.optional
)
312 frameEl
.lastChild
.appendChild(E('option', {
314 'selected': (this.values
.length
== 0 || this.values
[0] == '') ? '' : null
315 }, [ this.choices
[''] || this.options
.placeholder
|| _('-- Please choose --') ]));
317 for (var i
= 0; i
< keys
.length
; i
++) {
318 if (keys
[i
] == null || keys
[i
] == '')
321 frameEl
.lastChild
.appendChild(E('option', {
323 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
324 }, [ this.choices
[keys
[i
]] || keys
[i
] ]));
328 var brEl
= (this.options
.orientation
=== 'horizontal') ? document
.createTextNode(' ') : E('br');
330 for (var i
= 0; i
< keys
.length
; i
++) {
331 frameEl
.appendChild(E('label', {}, [
333 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
334 'name': this.options
.id
|| this.options
.name
,
335 'type': this.options
.multiple
? 'checkbox' : 'radio',
336 'class': this.options
.multiple
? 'cbi-input-checkbox' : 'cbi-input-radio',
338 'checked': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
340 this.choices
[keys
[i
]] || keys
[i
]
343 if (i
+ 1 == this.options
.size
)
344 frameEl
.appendChild(brEl
);
348 return this.bind(frameEl
);
351 bind: function(frameEl
) {
354 if (this.options
.widget
== 'select') {
355 this.setUpdateEvents(frameEl
.firstChild
, 'change', 'click', 'blur');
356 this.setChangeEvents(frameEl
.firstChild
, 'change');
359 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
360 for (var i
= 0; i
< radioEls
.length
; i
++) {
361 this.setUpdateEvents(radioEls
[i
], 'change', 'click', 'blur');
362 this.setChangeEvents(radioEls
[i
], 'change', 'click', 'blur');
366 L
.dom
.bindClassInstance(frameEl
, this);
371 getValue: function() {
372 if (this.options
.widget
== 'select')
373 return this.node
.firstChild
.value
;
375 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
376 for (var i
= 0; i
< radioEls
.length
; i
++)
377 if (radioEls
[i
].checked
)
378 return radioEls
[i
].value
;
383 setValue: function(value
) {
384 if (this.options
.widget
== 'select') {
388 for (var i
= 0; i
< this.node
.firstChild
.options
.length
; i
++)
389 this.node
.firstChild
.options
[i
].selected
= (this.node
.firstChild
.options
[i
].value
== value
);
394 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
395 for (var i
= 0; i
< radioEls
.length
; i
++)
396 radioEls
[i
].checked
= (radioEls
[i
].value
== value
);
400 var UIDropdown
= UIElement
.extend({
401 __init__: function(value
, choices
, options
) {
402 if (typeof(choices
) != 'object')
405 if (!Array
.isArray(value
))
406 this.values
= (value
!= null && value
!= '') ? [ value
] : [];
410 this.choices
= choices
;
411 this.options
= Object
.assign({
413 multiple
: Array
.isArray(value
),
415 select_placeholder
: _('-- Please choose --'),
416 custom_placeholder
: _('-- custom --'),
420 create_query
: '.create-item-input',
421 create_template
: 'script[type="item-template"]'
427 'id': this.options
.id
,
428 'class': 'cbi-dropdown',
429 'multiple': this.options
.multiple
? '' : null,
430 'optional': this.options
.optional
? '' : null,
433 var keys
= Object
.keys(this.choices
);
435 if (this.options
.sort
=== true)
437 else if (Array
.isArray(this.options
.sort
))
438 keys
= this.options
.sort
;
440 if (this.options
.create
)
441 for (var i
= 0; i
< this.values
.length
; i
++)
442 if (!this.choices
.hasOwnProperty(this.values
[i
]))
443 keys
.push(this.values
[i
]);
445 for (var i
= 0; i
< keys
.length
; i
++) {
446 var label
= this.choices
[keys
[i
]];
448 if (L
.dom
.elem(label
))
449 label
= label
.cloneNode(true);
451 sb
.lastElementChild
.appendChild(E('li', {
452 'data-value': keys
[i
],
453 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
454 }, [ label
|| keys
[i
] ]));
457 if (this.options
.create
) {
458 var createEl
= E('input', {
460 'class': 'create-item-input',
461 'readonly': this.options
.readonly
? '' : null,
462 'maxlength': this.options
.maxlength
,
463 'placeholder': this.options
.custom_placeholder
|| this.options
.placeholder
466 if (this.options
.datatype
|| this.options
.validate
)
467 L
.ui
.addValidator(createEl
, this.options
.datatype
|| 'string',
468 true, this.options
.validate
, 'blur', 'keyup');
470 sb
.lastElementChild
.appendChild(E('li', { 'data-value': '-' }, createEl
));
473 if (this.options
.create_markup
)
474 sb
.appendChild(E('script', { type
: 'item-template' },
475 this.options
.create_markup
));
477 return this.bind(sb
);
481 var o
= this.options
;
483 o
.multiple
= sb
.hasAttribute('multiple');
484 o
.optional
= sb
.hasAttribute('optional');
485 o
.placeholder
= sb
.getAttribute('placeholder') || o
.placeholder
;
486 o
.display_items
= parseInt(sb
.getAttribute('display-items') || o
.display_items
);
487 o
.dropdown_items
= parseInt(sb
.getAttribute('dropdown-items') || o
.dropdown_items
);
488 o
.create_query
= sb
.getAttribute('item-create') || o
.create_query
;
489 o
.create_template
= sb
.getAttribute('item-template') || o
.create_template
;
491 var ul
= sb
.querySelector('ul'),
492 more
= sb
.appendChild(E('span', { class: 'more', tabindex
: -1 }, '···')),
493 open
= sb
.appendChild(E('span', { class: 'open', tabindex
: -1 }, '▾')),
494 canary
= sb
.appendChild(E('div')),
495 create
= sb
.querySelector(this.options
.create_query
),
496 ndisplay
= this.options
.display_items
,
499 if (this.options
.multiple
) {
500 var items
= ul
.querySelectorAll('li');
502 for (var i
= 0; i
< items
.length
; i
++) {
503 this.transformItem(sb
, items
[i
]);
505 if (items
[i
].hasAttribute('selected') && ndisplay
-- > 0)
506 items
[i
].setAttribute('display', n
++);
510 if (this.options
.optional
&& !ul
.querySelector('li[data-value=""]')) {
511 var placeholder
= E('li', { placeholder
: '' },
512 this.options
.select_placeholder
|| this.options
.placeholder
);
515 ? ul
.insertBefore(placeholder
, ul
.firstChild
)
516 : ul
.appendChild(placeholder
);
519 var items
= ul
.querySelectorAll('li'),
520 sel
= sb
.querySelectorAll('[selected]');
522 sel
.forEach(function(s
) {
523 s
.removeAttribute('selected');
526 var s
= sel
[0] || items
[0];
528 s
.setAttribute('selected', '');
529 s
.setAttribute('display', n
++);
535 this.saveValues(sb
, ul
);
537 ul
.setAttribute('tabindex', -1);
538 sb
.setAttribute('tabindex', 0);
541 sb
.setAttribute('more', '')
543 sb
.removeAttribute('more');
545 if (ndisplay
== this.options
.display_items
)
546 sb
.setAttribute('empty', '')
548 sb
.removeAttribute('empty');
550 L
.dom
.content(more
, (ndisplay
== this.options
.display_items
)
551 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
554 sb
.addEventListener('click', this.handleClick
.bind(this));
555 sb
.addEventListener('keydown', this.handleKeydown
.bind(this));
556 sb
.addEventListener('cbi-dropdown-close', this.handleDropdownClose
.bind(this));
557 sb
.addEventListener('cbi-dropdown-select', this.handleDropdownSelect
.bind(this));
559 if ('ontouchstart' in window
) {
560 sb
.addEventListener('touchstart', function(ev
) { ev
.stopPropagation(); });
561 window
.addEventListener('touchstart', this.closeAllDropdowns
);
564 sb
.addEventListener('mouseover', this.handleMouseover
.bind(this));
565 sb
.addEventListener('focus', this.handleFocus
.bind(this));
567 canary
.addEventListener('focus', this.handleCanaryFocus
.bind(this));
569 window
.addEventListener('mouseover', this.setFocus
);
570 window
.addEventListener('click', this.closeAllDropdowns
);
574 create
.addEventListener('keydown', this.handleCreateKeydown
.bind(this));
575 create
.addEventListener('focus', this.handleCreateFocus
.bind(this));
576 create
.addEventListener('blur', this.handleCreateBlur
.bind(this));
578 var li
= findParent(create
, 'li');
580 li
.setAttribute('unselectable', '');
581 li
.addEventListener('click', this.handleCreateClick
.bind(this));
586 this.setUpdateEvents(sb
, 'cbi-dropdown-open', 'cbi-dropdown-close');
587 this.setChangeEvents(sb
, 'cbi-dropdown-change', 'cbi-dropdown-close');
589 L
.dom
.bindClassInstance(sb
, this);
594 openDropdown: function(sb
) {
595 var st
= window
.getComputedStyle(sb
, null),
596 ul
= sb
.querySelector('ul'),
597 li
= ul
.querySelectorAll('li'),
598 fl
= findParent(sb
, '.cbi-value-field'),
599 sel
= ul
.querySelector('[selected]'),
600 rect
= sb
.getBoundingClientRect(),
601 items
= Math
.min(this.options
.dropdown_items
, li
.length
);
603 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
604 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
607 sb
.setAttribute('open', '');
609 var pv
= ul
.cloneNode(true);
610 pv
.classList
.add('preview');
613 fl
.classList
.add('cbi-dropdown-open');
615 if ('ontouchstart' in window
) {
616 var vpWidth
= Math
.max(document
.documentElement
.clientWidth
, window
.innerWidth
|| 0),
617 vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
618 scrollFrom
= window
.pageYOffset
,
619 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
622 ul
.style
.top
= sb
.offsetHeight
+ 'px';
623 ul
.style
.left
= -rect
.left
+ 'px';
624 ul
.style
.right
= (rect
.right
- vpWidth
) + 'px';
625 ul
.style
.maxHeight
= (vpHeight
* 0.5) + 'px';
626 ul
.style
.WebkitOverflowScrolling
= 'touch';
628 var scrollStep = function(timestamp
) {
631 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
634 var duration
= Math
.max(timestamp
- start
, 1);
635 if (duration
< 100) {
636 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
637 window
.requestAnimationFrame(scrollStep
);
640 document
.body
.scrollTop
= scrollTo
;
644 window
.requestAnimationFrame(scrollStep
);
647 ul
.style
.maxHeight
= '1px';
648 ul
.style
.top
= ul
.style
.bottom
= '';
650 window
.requestAnimationFrame(function() {
651 var itemHeight
= li
[Math
.max(0, li
.length
- 2)].getBoundingClientRect().height
,
653 spaceAbove
= rect
.top
,
654 spaceBelow
= window
.innerHeight
- rect
.height
- rect
.top
;
656 for (var i
= 0; i
< (items
== -1 ? li
.length
: items
); i
++)
657 fullHeight
+= li
[i
].getBoundingClientRect().height
;
659 if (fullHeight
<= spaceBelow
) {
660 ul
.style
.top
= rect
.height
+ 'px';
661 ul
.style
.maxHeight
= spaceBelow
+ 'px';
663 else if (fullHeight
<= spaceAbove
) {
664 ul
.style
.bottom
= rect
.height
+ 'px';
665 ul
.style
.maxHeight
= spaceAbove
+ 'px';
667 else if (spaceBelow
>= spaceAbove
) {
668 ul
.style
.top
= rect
.height
+ 'px';
669 ul
.style
.maxHeight
= (spaceBelow
- (spaceBelow
% itemHeight
)) + 'px';
672 ul
.style
.bottom
= rect
.height
+ 'px';
673 ul
.style
.maxHeight
= (spaceAbove
- (spaceAbove
% itemHeight
)) + 'px';
676 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
680 var cboxes
= ul
.querySelectorAll('[selected] input[type="checkbox"]');
681 for (var i
= 0; i
< cboxes
.length
; i
++) {
682 cboxes
[i
].checked
= true;
683 cboxes
[i
].disabled
= (cboxes
.length
== 1 && !this.options
.optional
);
686 ul
.classList
.add('dropdown');
688 sb
.insertBefore(pv
, ul
.nextElementSibling
);
690 li
.forEach(function(l
) {
691 l
.setAttribute('tabindex', 0);
694 sb
.lastElementChild
.setAttribute('tabindex', 0);
696 this.setFocus(sb
, sel
|| li
[0], true);
699 closeDropdown: function(sb
, no_focus
) {
700 if (!sb
.hasAttribute('open'))
703 var pv
= sb
.querySelector('ul.preview'),
704 ul
= sb
.querySelector('ul.dropdown'),
705 li
= ul
.querySelectorAll('li'),
706 fl
= findParent(sb
, '.cbi-value-field');
708 li
.forEach(function(l
) { l
.removeAttribute('tabindex'); });
709 sb
.lastElementChild
.removeAttribute('tabindex');
712 sb
.removeAttribute('open');
713 sb
.style
.width
= sb
.style
.height
= '';
715 ul
.classList
.remove('dropdown');
716 ul
.style
.top
= ul
.style
.bottom
= ul
.style
.maxHeight
= '';
719 fl
.classList
.remove('cbi-dropdown-open');
722 this.setFocus(sb
, sb
);
724 this.saveValues(sb
, ul
);
727 toggleItem: function(sb
, li
, force_state
) {
728 if (li
.hasAttribute('unselectable'))
731 if (this.options
.multiple
) {
732 var cbox
= li
.querySelector('input[type="checkbox"]'),
733 items
= li
.parentNode
.querySelectorAll('li'),
734 label
= sb
.querySelector('ul.preview'),
735 sel
= li
.parentNode
.querySelectorAll('[selected]').length
,
736 more
= sb
.querySelector('.more'),
737 ndisplay
= this.options
.display_items
,
740 if (li
.hasAttribute('selected')) {
741 if (force_state
!== true) {
742 if (sel
> 1 || this.options
.optional
) {
743 li
.removeAttribute('selected');
744 cbox
.checked
= cbox
.disabled
= false;
748 cbox
.disabled
= true;
753 if (force_state
!== false) {
754 li
.setAttribute('selected', '');
756 cbox
.disabled
= false;
761 while (label
&& label
.firstElementChild
)
762 label
.removeChild(label
.firstElementChild
);
764 for (var i
= 0; i
< items
.length
; i
++) {
765 items
[i
].removeAttribute('display');
766 if (items
[i
].hasAttribute('selected')) {
767 if (ndisplay
-- > 0) {
768 items
[i
].setAttribute('display', n
++);
770 label
.appendChild(items
[i
].cloneNode(true));
772 var c
= items
[i
].querySelector('input[type="checkbox"]');
774 c
.disabled
= (sel
== 1 && !this.options
.optional
);
779 sb
.setAttribute('more', '');
781 sb
.removeAttribute('more');
783 if (ndisplay
=== this.options
.display_items
)
784 sb
.setAttribute('empty', '');
786 sb
.removeAttribute('empty');
788 L
.dom
.content(more
, (ndisplay
=== this.options
.display_items
)
789 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
792 var sel
= li
.parentNode
.querySelector('[selected]');
794 sel
.removeAttribute('display');
795 sel
.removeAttribute('selected');
798 li
.setAttribute('display', 0);
799 li
.setAttribute('selected', '');
801 this.closeDropdown(sb
, true);
804 this.saveValues(sb
, li
.parentNode
);
807 transformItem: function(sb
, li
) {
808 var cbox
= E('form', {}, E('input', { type
: 'checkbox', tabindex
: -1, onclick
: 'event.preventDefault()' })),
811 while (li
.firstChild
)
812 label
.appendChild(li
.firstChild
);
814 li
.appendChild(cbox
);
815 li
.appendChild(label
);
818 saveValues: function(sb
, ul
) {
819 var sel
= ul
.querySelectorAll('li[selected]'),
820 div
= sb
.lastElementChild
,
821 name
= this.options
.name
,
825 while (div
.lastElementChild
)
826 div
.removeChild(div
.lastElementChild
);
828 sel
.forEach(function (s
) {
829 if (s
.hasAttribute('placeholder'))
834 value
: s
.hasAttribute('data-value') ? s
.getAttribute('data-value') : s
.innerText
,
838 div
.appendChild(E('input', {
846 strval
+= strval
.length
? ' ' + v
.value
: v
.value
;
854 if (this.options
.multiple
)
855 detail
.values
= values
;
857 detail
.value
= values
.length
? values
[0] : null;
861 sb
.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
867 setValues: function(sb
, values
) {
868 var ul
= sb
.querySelector('ul');
870 if (this.options
.create
) {
871 for (var value
in values
) {
872 this.createItems(sb
, value
);
874 if (!this.options
.multiple
)
879 if (this.options
.multiple
) {
880 var lis
= ul
.querySelectorAll('li[data-value]');
881 for (var i
= 0; i
< lis
.length
; i
++) {
882 var value
= lis
[i
].getAttribute('data-value');
883 if (values
=== null || !(value
in values
))
884 this.toggleItem(sb
, lis
[i
], false);
886 this.toggleItem(sb
, lis
[i
], true);
890 var ph
= ul
.querySelector('li[placeholder]');
892 this.toggleItem(sb
, ph
);
894 var lis
= ul
.querySelectorAll('li[data-value]');
895 for (var i
= 0; i
< lis
.length
; i
++) {
896 var value
= lis
[i
].getAttribute('data-value');
897 if (values
!== null && (value
in values
))
898 this.toggleItem(sb
, lis
[i
]);
903 setFocus: function(sb
, elem
, scroll
) {
904 if (sb
&& sb
.hasAttribute
&& sb
.hasAttribute('locked-in'))
907 if (sb
.target
&& findParent(sb
.target
, 'ul.dropdown'))
910 document
.querySelectorAll('.focus').forEach(function(e
) {
911 if (!matchesElem(e
, 'input')) {
912 e
.classList
.remove('focus');
919 elem
.classList
.add('focus');
922 elem
.parentNode
.scrollTop
= elem
.offsetTop
- elem
.parentNode
.offsetTop
;
926 createChoiceElement: function(sb
, value
, label
) {
927 var tpl
= sb
.querySelector(this.options
.create_template
),
931 markup
= (tpl
.textContent
|| tpl
.innerHTML
|| tpl
.firstChild
.data
).replace(/^<!--|-->$/, '').trim();
933 markup
= '<li data-value="{{value}}"><span data-label-placeholder="true" /></li>';
935 var new_item
= E(markup
.replace(/{{value}}/g, '%h'.format(value
))),
936 placeholder
= new_item
.querySelector('[data-label-placeholder]');
939 var content
= E('span', {}, label
|| this.choices
[value
] || [ value
]);
941 while (content
.firstChild
)
942 placeholder
.parentNode
.insertBefore(content
.firstChild
, placeholder
);
944 placeholder
.parentNode
.removeChild(placeholder
);
947 if (this.options
.multiple
)
948 this.transformItem(sb
, new_item
);
953 createItems: function(sb
, value
) {
955 val
= (value
|| '').trim(),
956 ul
= sb
.querySelector('ul');
958 if (!sbox
.options
.multiple
)
959 val
= val
.length
? [ val
] : [];
961 val
= val
.length
? val
.split(/\s+/) : [];
963 val
.forEach(function(item
) {
966 ul
.childNodes
.forEach(function(li
) {
967 if (li
.getAttribute
&& li
.getAttribute('data-value') === item
)
972 new_item
= sbox
.createChoiceElement(sb
, item
);
974 if (!sbox
.options
.multiple
) {
975 var old
= ul
.querySelector('li[created]');
979 new_item
.setAttribute('created', '');
982 new_item
= ul
.insertBefore(new_item
, ul
.lastElementChild
);
985 sbox
.toggleItem(sb
, new_item
, true);
986 sbox
.setFocus(sb
, new_item
, true);
990 clearChoices: function(reset_value
) {
991 var ul
= this.node
.querySelector('ul'),
992 lis
= ul
? ul
.querySelectorAll('li[data-value]') : [],
993 len
= lis
.length
- (this.options
.create
? 1 : 0),
994 val
= reset_value
? null : this.getValue();
996 for (var i
= 0; i
< len
; i
++) {
997 var lival
= lis
[i
].getAttribute('data-value');
999 (!this.options
.multiple
&& val
!= lival
) ||
1000 (this.options
.multiple
&& val
.indexOf(lival
) == -1))
1001 ul
.removeChild(lis
[i
]);
1005 this.setValues(this.node
, {});
1008 addChoices: function(values
, labels
) {
1010 ul
= sb
.querySelector('ul'),
1011 lis
= ul
? ul
.querySelectorAll('li[data-value]') : [];
1013 if (!Array
.isArray(values
))
1014 values
= L
.toArray(values
);
1016 if (!L
.isObject(labels
))
1019 for (var i
= 0; i
< values
.length
; i
++) {
1022 for (var j
= 0; j
< lis
.length
; j
++) {
1023 if (lis
[j
].getAttribute('data-value') === values
[i
]) {
1033 this.createChoiceElement(sb
, values
[i
], labels
[values
[i
]]),
1034 ul
.lastElementChild
);
1038 closeAllDropdowns: function() {
1039 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1040 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1044 handleClick: function(ev
) {
1045 var sb
= ev
.currentTarget
;
1047 if (!sb
.hasAttribute('open')) {
1048 if (!matchesElem(ev
.target
, 'input'))
1049 this.openDropdown(sb
);
1052 var li
= findParent(ev
.target
, 'li');
1053 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1054 this.toggleItem(sb
, li
);
1055 else if (li
&& li
.parentNode
.classList
.contains('preview'))
1056 this.closeDropdown(sb
);
1057 else if (matchesElem(ev
.target
, 'span.open, span.more'))
1058 this.closeDropdown(sb
);
1061 ev
.preventDefault();
1062 ev
.stopPropagation();
1065 handleKeydown: function(ev
) {
1066 var sb
= ev
.currentTarget
;
1068 if (matchesElem(ev
.target
, 'input'))
1071 if (!sb
.hasAttribute('open')) {
1072 switch (ev
.keyCode
) {
1077 this.openDropdown(sb
);
1078 ev
.preventDefault();
1082 var active
= findParent(document
.activeElement
, 'li');
1084 switch (ev
.keyCode
) {
1086 this.closeDropdown(sb
);
1091 if (!active
.hasAttribute('selected'))
1092 this.toggleItem(sb
, active
);
1093 this.closeDropdown(sb
);
1094 ev
.preventDefault();
1100 this.toggleItem(sb
, active
);
1101 ev
.preventDefault();
1106 if (active
&& active
.previousElementSibling
) {
1107 this.setFocus(sb
, active
.previousElementSibling
);
1108 ev
.preventDefault();
1113 if (active
&& active
.nextElementSibling
) {
1114 this.setFocus(sb
, active
.nextElementSibling
);
1115 ev
.preventDefault();
1122 handleDropdownClose: function(ev
) {
1123 var sb
= ev
.currentTarget
;
1125 this.closeDropdown(sb
, true);
1128 handleDropdownSelect: function(ev
) {
1129 var sb
= ev
.currentTarget
,
1130 li
= findParent(ev
.target
, 'li');
1135 this.toggleItem(sb
, li
);
1136 this.closeDropdown(sb
, true);
1139 handleMouseover: function(ev
) {
1140 var sb
= ev
.currentTarget
;
1142 if (!sb
.hasAttribute('open'))
1145 var li
= findParent(ev
.target
, 'li');
1147 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1148 this.setFocus(sb
, li
);
1151 handleFocus: function(ev
) {
1152 var sb
= ev
.currentTarget
;
1154 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1155 if (s
!== sb
|| sb
.hasAttribute('open'))
1156 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1160 handleCanaryFocus: function(ev
) {
1161 this.closeDropdown(ev
.currentTarget
.parentNode
);
1164 handleCreateKeydown: function(ev
) {
1165 var input
= ev
.currentTarget
,
1166 sb
= findParent(input
, '.cbi-dropdown');
1168 switch (ev
.keyCode
) {
1170 ev
.preventDefault();
1172 if (input
.classList
.contains('cbi-input-invalid'))
1175 this.createItems(sb
, input
.value
);
1182 handleCreateFocus: function(ev
) {
1183 var input
= ev
.currentTarget
,
1184 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1185 sb
= findParent(input
, '.cbi-dropdown');
1188 cbox
.checked
= true;
1190 sb
.setAttribute('locked-in', '');
1193 handleCreateBlur: function(ev
) {
1194 var input
= ev
.currentTarget
,
1195 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1196 sb
= findParent(input
, '.cbi-dropdown');
1199 cbox
.checked
= false;
1201 sb
.removeAttribute('locked-in');
1204 handleCreateClick: function(ev
) {
1205 ev
.currentTarget
.querySelector(this.options
.create_query
).focus();
1208 setValue: function(values
) {
1209 if (this.options
.multiple
) {
1210 if (!Array
.isArray(values
))
1211 values
= (values
!= null && values
!= '') ? [ values
] : [];
1215 for (var i
= 0; i
< values
.length
; i
++)
1216 v
[values
[i
]] = true;
1218 this.setValues(this.node
, v
);
1223 if (values
!= null) {
1224 if (Array
.isArray(values
))
1225 v
[values
[0]] = true;
1230 this.setValues(this.node
, v
);
1234 getValue: function() {
1235 var div
= this.node
.lastElementChild
,
1236 h
= div
.querySelectorAll('input[type="hidden"]'),
1239 for (var i
= 0; i
< h
.length
; i
++)
1242 return this.options
.multiple
? v
: v
[0];
1246 var UICombobox
= UIDropdown
.extend({
1247 __init__: function(value
, choices
, options
) {
1248 this.super('__init__', [ value
, choices
, Object
.assign({
1249 select_placeholder
: _('-- Please choose --'),
1250 custom_placeholder
: _('-- custom --'),
1261 var UIComboButton
= UIDropdown
.extend({
1262 __init__: function(value
, choices
, options
) {
1263 this.super('__init__', [ value
, choices
, Object
.assign({
1272 render: function(/* ... */) {
1273 var node
= UIDropdown
.prototype.render
.apply(this, arguments
),
1274 val
= this.getValue();
1276 if (L
.isObject(this.options
.classes
) && this.options
.classes
.hasOwnProperty(val
))
1277 node
.setAttribute('class', 'cbi-dropdown ' + this.options
.classes
[val
]);
1282 handleClick: function(ev
) {
1283 var sb
= ev
.currentTarget
,
1286 if (sb
.hasAttribute('open') || L
.dom
.matches(t
, '.cbi-dropdown > span.open'))
1287 return UIDropdown
.prototype.handleClick
.apply(this, arguments
);
1289 if (this.options
.click
)
1290 return this.options
.click
.call(sb
, ev
, this.getValue());
1293 toggleItem: function(sb
/*, ... */) {
1294 var rv
= UIDropdown
.prototype.toggleItem
.apply(this, arguments
),
1295 val
= this.getValue();
1297 if (L
.isObject(this.options
.classes
) && this.options
.classes
.hasOwnProperty(val
))
1298 sb
.setAttribute('class', 'cbi-dropdown ' + this.options
.classes
[val
]);
1300 sb
.setAttribute('class', 'cbi-dropdown');
1306 var UIDynamicList
= UIElement
.extend({
1307 __init__: function(values
, choices
, options
) {
1308 if (!Array
.isArray(values
))
1309 values
= (values
!= null && values
!= '') ? [ values
] : [];
1311 if (typeof(choices
) != 'object')
1314 this.values
= values
;
1315 this.choices
= choices
;
1316 this.options
= Object
.assign({}, options
, {
1322 render: function() {
1324 'id': this.options
.id
,
1325 'class': 'cbi-dynlist'
1326 }, E('div', { 'class': 'add-item' }));
1329 if (this.options
.placeholder
!= null)
1330 this.options
.select_placeholder
= this.options
.placeholder
;
1332 var cbox
= new UICombobox(null, this.choices
, this.options
);
1334 dl
.lastElementChild
.appendChild(cbox
.render());
1337 var inputEl
= E('input', {
1338 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1340 'class': 'cbi-input-text',
1341 'placeholder': this.options
.placeholder
1344 dl
.lastElementChild
.appendChild(inputEl
);
1345 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1347 if (this.options
.datatype
|| this.options
.validate
)
1348 L
.ui
.addValidator(inputEl
, this.options
.datatype
|| 'string',
1349 true, this.options
.validate
, 'blur', 'keyup');
1352 for (var i
= 0; i
< this.values
.length
; i
++) {
1353 var label
= this.choices
? this.choices
[this.values
[i
]] : null;
1355 if (L
.dom
.elem(label
))
1356 label
= label
.cloneNode(true);
1358 this.addItem(dl
, this.values
[i
], label
);
1361 return this.bind(dl
);
1364 bind: function(dl
) {
1365 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1366 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1367 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1371 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1372 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1374 L
.dom
.bindClassInstance(dl
, this);
1379 addItem: function(dl
, value
, text
, flash
) {
1381 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1382 E('span', {}, [ text
|| value
]),
1385 'name': this.options
.name
,
1386 'value': value
})]);
1388 dl
.querySelectorAll('.item').forEach(function(item
) {
1392 var hidden
= item
.querySelector('input[type="hidden"]');
1394 if (hidden
&& hidden
.parentNode
!== item
)
1397 if (hidden
&& hidden
.value
=== value
)
1402 var ai
= dl
.querySelector('.add-item');
1403 ai
.parentNode
.insertBefore(new_item
, ai
);
1406 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1417 removeItem: function(dl
, item
) {
1418 var value
= item
.querySelector('input[type="hidden"]').value
;
1419 var sb
= dl
.querySelector('.cbi-dropdown');
1421 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1422 if (li
.getAttribute('data-value') === value
) {
1423 if (li
.hasAttribute('dynlistcustom'))
1424 li
.parentNode
.removeChild(li
);
1426 li
.removeAttribute('unselectable');
1430 item
.parentNode
.removeChild(item
);
1432 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1443 handleClick: function(ev
) {
1444 var dl
= ev
.currentTarget
,
1445 item
= findParent(ev
.target
, '.item');
1448 this.removeItem(dl
, item
);
1450 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1451 var input
= ev
.target
.previousElementSibling
;
1452 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1453 this.addItem(dl
, input
.value
, null, true);
1459 handleDropdownChange: function(ev
) {
1460 var dl
= ev
.currentTarget
,
1461 sbIn
= ev
.detail
.instance
,
1462 sbEl
= ev
.detail
.element
,
1463 sbVal
= ev
.detail
.value
;
1468 sbIn
.setValues(sbEl
, null);
1469 sbVal
.element
.setAttribute('unselectable', '');
1471 if (sbVal
.element
.hasAttribute('created')) {
1472 sbVal
.element
.removeAttribute('created');
1473 sbVal
.element
.setAttribute('dynlistcustom', '');
1476 var label
= sbVal
.text
;
1478 if (sbVal
.element
) {
1481 for (var i
= 0; i
< sbVal
.element
.childNodes
.length
; i
++)
1482 label
.appendChild(sbVal
.element
.childNodes
[i
].cloneNode(true));
1485 this.addItem(dl
, sbVal
.value
, label
, true);
1488 handleKeydown: function(ev
) {
1489 var dl
= ev
.currentTarget
,
1490 item
= findParent(ev
.target
, '.item');
1493 switch (ev
.keyCode
) {
1494 case 8: /* backspace */
1495 if (item
.previousElementSibling
)
1496 item
.previousElementSibling
.focus();
1498 this.removeItem(dl
, item
);
1501 case 46: /* delete */
1502 if (item
.nextElementSibling
) {
1503 if (item
.nextElementSibling
.classList
.contains('item'))
1504 item
.nextElementSibling
.focus();
1506 item
.nextElementSibling
.firstElementChild
.focus();
1509 this.removeItem(dl
, item
);
1513 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1514 switch (ev
.keyCode
) {
1515 case 13: /* enter */
1516 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1517 this.addItem(dl
, ev
.target
.value
, null, true);
1518 ev
.target
.value
= '';
1523 ev
.preventDefault();
1529 getValue: function() {
1530 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1531 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1534 for (var i
= 0; i
< items
.length
; i
++)
1535 v
.push(items
[i
].value
);
1537 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1538 input
.classList
.contains('cbi-input-invalid') == false &&
1539 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1540 v
.push(input
.value
);
1545 setValue: function(values
) {
1546 if (!Array
.isArray(values
))
1547 values
= (values
!= null && values
!= '') ? [ values
] : [];
1549 var items
= this.node
.querySelectorAll('.item');
1551 for (var i
= 0; i
< items
.length
; i
++)
1552 if (items
[i
].parentNode
=== this.node
)
1553 this.removeItem(this.node
, items
[i
]);
1555 for (var i
= 0; i
< values
.length
; i
++)
1556 this.addItem(this.node
, values
[i
],
1557 this.choices
? this.choices
[values
[i
]] : null);
1560 addChoices: function(values
, labels
) {
1561 var dl
= this.node
.lastElementChild
.firstElementChild
;
1562 L
.dom
.callClassMethod(dl
, 'addChoices', values
, labels
);
1565 clearChoices: function() {
1566 var dl
= this.node
.lastElementChild
.firstElementChild
;
1567 L
.dom
.callClassMethod(dl
, 'clearChoices');
1571 var UIHiddenfield
= UIElement
.extend({
1572 __init__: function(value
, options
) {
1574 this.options
= Object
.assign({
1579 render: function() {
1580 var hiddenEl
= E('input', {
1581 'id': this.options
.id
,
1586 return this.bind(hiddenEl
);
1589 bind: function(hiddenEl
) {
1590 this.node
= hiddenEl
;
1592 L
.dom
.bindClassInstance(hiddenEl
, this);
1597 getValue: function() {
1598 return this.node
.value
;
1601 setValue: function(value
) {
1602 this.node
.value
= value
;
1606 var UIFileUpload
= UIElement
.extend({
1607 __init__: function(value
, options
) {
1609 this.options
= Object
.assign({
1611 enable_upload
: true,
1612 enable_remove
: true,
1613 root_directory
: '/etc/luci-uploads'
1617 bind: function(browserEl
) {
1618 this.node
= browserEl
;
1620 this.setUpdateEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1621 this.setChangeEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1623 L
.dom
.bindClassInstance(browserEl
, this);
1628 render: function() {
1629 return L
.resolveDefault(this.value
!= null ? fs
.stat(this.value
) : null).then(L
.bind(function(stat
) {
1632 if (L
.isObject(stat
) && stat
.type
!= 'directory')
1635 if (this.stat
!= null)
1636 label
= [ this.iconForType(this.stat
.type
), ' %s (%1000mB)'.format(this.truncatePath(this.stat
.path
), this.stat
.size
) ];
1637 else if (this.value
!= null)
1638 label
= [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value
), _('File not accessible')) ];
1640 label
= [ _('Select file…') ];
1642 return this.bind(E('div', { 'id': this.options
.id
}, [
1645 'click': L
.ui
.createHandlerFn(this, 'handleFileBrowser')
1648 'class': 'cbi-filebrowser'
1652 'name': this.options
.name
,
1659 truncatePath: function(path
) {
1660 if (path
.length
> 50)
1661 path
= path
.substring(0, 25) + '…' + path
.substring(path
.length
- 25);
1666 iconForType: function(type
) {
1670 'src': L
.resource('cbi/link.gif'),
1671 'title': _('Symbolic link'),
1677 'src': L
.resource('cbi/folder.gif'),
1678 'title': _('Directory'),
1684 'src': L
.resource('cbi/file.gif'),
1691 canonicalizePath: function(path
) {
1692 return path
.replace(/\/{2,}/, '/')
1693 .replace(/\/\.(\/|$)/g, '/')
1694 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1695 .replace(/\/$/, '');
1698 splitPath: function(path
) {
1699 var croot
= this.canonicalizePath(this.options
.root_directory
|| '/'),
1700 cpath
= this.canonicalizePath(path
|| '/');
1702 if (cpath
.length
<= croot
.length
)
1705 if (cpath
.charAt(croot
.length
) != '/')
1708 var parts
= cpath
.substring(croot
.length
+ 1).split(/\//);
1710 parts
.unshift(croot
);
1715 handleUpload: function(path
, list
, ev
) {
1716 var form
= ev
.target
.parentNode
,
1717 fileinput
= form
.querySelector('input[type="file"]'),
1718 nameinput
= form
.querySelector('input[type="text"]'),
1719 filename
= (nameinput
.value
!= null ? nameinput
.value
: '').trim();
1721 ev
.preventDefault();
1723 if (filename
== '' || filename
.match(/\//) || fileinput
.files
[0] == null)
1726 var existing
= list
.filter(function(e
) { return e
.name
== filename
})[0];
1728 if (existing
!= null && existing
.type
== 'directory')
1729 return alert(_('A directory with the same name already exists.'));
1730 else if (existing
!= null && !confirm(_('Overwrite existing file "%s" ?').format(filename
)))
1733 var data
= new FormData();
1735 data
.append('sessionid', L
.env
.sessionid
);
1736 data
.append('filename', path
+ '/' + filename
);
1737 data
.append('filedata', fileinput
.files
[0]);
1739 return L
.Request
.post(L
.env
.cgi_base
+ '/cgi-upload', data
, {
1740 progress
: L
.bind(function(btn
, ev
) {
1741 btn
.firstChild
.data
= '%.2f%%'.format((ev
.loaded
/ ev
.total
) * 100);
1743 }).then(L
.bind(function(path
, ev
, res
) {
1744 var reply
= res
.json();
1746 if (L
.isObject(reply
) && reply
.failure
)
1747 alert(_('Upload request failed: %s').format(reply
.message
));
1749 return this.handleSelect(path
, null, ev
);
1750 }, this, path
, ev
));
1753 handleDelete: function(path
, fileStat
, ev
) {
1754 var parent
= path
.replace(/\/[^\/]+$/, '') || '/',
1755 name
= path
.replace(/^.+\//, ''),
1758 ev
.preventDefault();
1760 if (fileStat
.type
== 'directory')
1761 msg
= _('Do you really want to recursively delete the directory "%s" ?').format(name
);
1763 msg
= _('Do you really want to delete "%s" ?').format(name
);
1766 var button
= this.node
.firstElementChild
,
1767 hidden
= this.node
.lastElementChild
;
1769 if (path
== hidden
.value
) {
1770 L
.dom
.content(button
, _('Select file…'));
1774 return fs
.remove(path
).then(L
.bind(function(parent
, ev
) {
1775 return this.handleSelect(parent
, null, ev
);
1776 }, this, parent
, ev
)).catch(function(err
) {
1777 alert(_('Delete request failed: %s').format(err
.message
));
1782 renderUpload: function(path
, list
) {
1783 if (!this.options
.enable_upload
)
1789 'class': 'btn cbi-button-positive',
1790 'click': function(ev
) {
1791 var uploadForm
= ev
.target
.nextElementSibling
,
1792 fileInput
= uploadForm
.querySelector('input[type="file"]');
1794 ev
.target
.style
.display
= 'none';
1795 uploadForm
.style
.display
= '';
1798 }, _('Upload file…')),
1799 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1802 'style': 'display:none',
1803 'change': function(ev
) {
1804 var nameinput
= ev
.target
.parentNode
.querySelector('input[type="text"]'),
1805 uploadbtn
= ev
.target
.parentNode
.querySelector('button.cbi-button-save');
1807 nameinput
.value
= ev
.target
.value
.replace(/^.+[\/\\]/, '');
1808 uploadbtn
.disabled
= false;
1813 'click': function(ev
) {
1814 ev
.preventDefault();
1815 ev
.target
.previousElementSibling
.click();
1817 }, [ _('Browse…') ]),
1818 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1820 'class': 'btn cbi-button-save',
1821 'click': L
.ui
.createHandlerFn(this, 'handleUpload', path
, list
),
1823 }, [ _('Upload file') ])
1828 renderListing: function(container
, path
, list
) {
1829 var breadcrumb
= E('p'),
1832 list
.sort(function(a
, b
) {
1833 var isDirA
= (a
.type
== 'directory'),
1834 isDirB
= (b
.type
== 'directory');
1836 if (isDirA
!= isDirB
)
1837 return isDirA
< isDirB
;
1839 return a
.name
> b
.name
;
1842 for (var i
= 0; i
< list
.length
; i
++) {
1843 if (!this.options
.show_hidden
&& list
[i
].name
.charAt(0) == '.')
1846 var entrypath
= this.canonicalizePath(path
+ '/' + list
[i
].name
),
1847 selected
= (entrypath
== this.node
.lastElementChild
.value
),
1848 mtime
= new Date(list
[i
].mtime
* 1000);
1850 rows
.appendChild(E('li', [
1851 E('div', { 'class': 'name' }, [
1852 this.iconForType(list
[i
].type
),
1856 'style': selected
? 'font-weight:bold' : null,
1857 'click': L
.ui
.createHandlerFn(this, 'handleSelect',
1858 entrypath
, list
[i
].type
!= 'directory' ? list
[i
] : null)
1859 }, '%h'.format(list
[i
].name
))
1861 E('div', { 'class': 'mtime hide-xs' }, [
1862 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1863 mtime
.getFullYear(),
1864 mtime
.getMonth() + 1,
1871 selected
? E('button', {
1873 'click': L
.ui
.createHandlerFn(this, 'handleReset')
1874 }, [ _('Deselect') ]) : '',
1875 this.options
.enable_remove
? E('button', {
1876 'class': 'btn cbi-button-negative',
1877 'click': L
.ui
.createHandlerFn(this, 'handleDelete', entrypath
, list
[i
])
1878 }, [ _('Delete') ]) : ''
1883 if (!rows
.firstElementChild
)
1884 rows
.appendChild(E('em', _('No entries in this directory')));
1886 var dirs
= this.splitPath(path
),
1889 for (var i
= 0; i
< dirs
.length
; i
++) {
1890 cur
= cur
? cur
+ '/' + dirs
[i
] : dirs
[i
];
1891 L
.dom
.append(breadcrumb
, [
1895 'click': L
.ui
.createHandlerFn(this, 'handleSelect', cur
|| '/', null)
1896 }, dirs
[i
] != '' ? '%h'.format(dirs
[i
]) : E('em', '(root)')),
1900 L
.dom
.content(container
, [
1903 E('div', { 'class': 'right' }, [
1904 this.renderUpload(path
, list
),
1908 'click': L
.ui
.createHandlerFn(this, 'handleCancel')
1914 handleCancel: function(ev
) {
1915 var button
= this.node
.firstElementChild
,
1916 browser
= button
.nextElementSibling
;
1918 browser
.classList
.remove('open');
1919 button
.style
.display
= '';
1921 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1923 ev
.preventDefault();
1926 handleReset: function(ev
) {
1927 var button
= this.node
.firstElementChild
,
1928 hidden
= this.node
.lastElementChild
;
1931 L
.dom
.content(button
, _('Select file…'));
1933 this.handleCancel(ev
);
1936 handleSelect: function(path
, fileStat
, ev
) {
1937 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1938 ul
= browser
.querySelector('ul');
1940 if (fileStat
== null) {
1941 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1942 L
.resolveDefault(fs
.list(path
), []).then(L
.bind(this.renderListing
, this, browser
, path
));
1945 var button
= this.node
.firstElementChild
,
1946 hidden
= this.node
.lastElementChild
;
1948 path
= this.canonicalizePath(path
);
1950 L
.dom
.content(button
, [
1951 this.iconForType(fileStat
.type
),
1952 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1955 browser
.classList
.remove('open');
1956 button
.style
.display
= '';
1957 hidden
.value
= path
;
1959 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1960 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1964 handleFileBrowser: function(ev
) {
1965 var button
= ev
.target
,
1966 browser
= button
.nextElementSibling
,
1967 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : (this.options
.initial_directory
|| this.options
.root_directory
);
1969 if (path
.indexOf(this.options
.root_directory
) != 0)
1970 path
= this.options
.root_directory
;
1972 ev
.preventDefault();
1974 return L
.resolveDefault(fs
.list(path
), []).then(L
.bind(function(button
, browser
, path
, list
) {
1975 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1976 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1979 button
.style
.display
= 'none';
1980 browser
.classList
.add('open');
1982 return this.renderListing(browser
, path
, list
);
1983 }, this, button
, browser
, path
));
1986 getValue: function() {
1987 return this.node
.lastElementChild
.value
;
1990 setValue: function(value
) {
1991 this.node
.lastElementChild
.value
= value
;
1996 return L
.Class
.extend({
1997 __init__: function() {
1998 modalDiv
= document
.body
.appendChild(
1999 L
.dom
.create('div', { id
: 'modal_overlay' },
2000 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
2002 tooltipDiv
= document
.body
.appendChild(
2003 L
.dom
.create('div', { class: 'cbi-tooltip' }));
2005 /* setup old aliases */
2006 L
.showModal
= this.showModal
;
2007 L
.hideModal
= this.hideModal
;
2008 L
.showTooltip
= this.showTooltip
;
2009 L
.hideTooltip
= this.hideTooltip
;
2010 L
.itemlist
= this.itemlist
;
2012 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
2013 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
2014 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
2015 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
2017 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
2018 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
2019 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
2023 showModal: function(title
, children
/* , ... */) {
2024 var dlg
= modalDiv
.firstElementChild
;
2026 dlg
.setAttribute('class', 'modal');
2028 for (var i
= 2; i
< arguments
.length
; i
++)
2029 dlg
.classList
.add(arguments
[i
]);
2031 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
2032 L
.dom
.append(dlg
, children
);
2034 document
.body
.classList
.add('modal-overlay-active');
2039 hideModal: function() {
2040 document
.body
.classList
.remove('modal-overlay-active');
2044 showTooltip: function(ev
) {
2045 var target
= findParent(ev
.target
, '[data-tooltip]');
2050 if (tooltipTimeout
!== null) {
2051 window
.clearTimeout(tooltipTimeout
);
2052 tooltipTimeout
= null;
2055 var rect
= target
.getBoundingClientRect(),
2056 x
= rect
.left
+ window
.pageXOffset
,
2057 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
2059 tooltipDiv
.className
= 'cbi-tooltip';
2060 tooltipDiv
.innerHTML
= '▲ ';
2061 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
2063 if (target
.hasAttribute('data-tooltip-style'))
2064 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
2066 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
2067 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
2068 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
2071 tooltipDiv
.style
.top
= y
+ 'px';
2072 tooltipDiv
.style
.left
= x
+ 'px';
2073 tooltipDiv
.style
.opacity
= 1;
2075 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
2077 detail
: { target
: target
}
2081 hideTooltip: function(ev
) {
2082 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
2083 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
2086 if (tooltipTimeout
!== null) {
2087 window
.clearTimeout(tooltipTimeout
);
2088 tooltipTimeout
= null;
2091 tooltipDiv
.style
.opacity
= 0;
2092 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
2094 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
2097 addNotification: function(title
, children
/*, ... */) {
2098 var mc
= document
.querySelector('#maincontent') || document
.body
;
2099 var msg
= E('div', {
2100 'class': 'alert-message fade-in',
2101 'style': 'display:flex',
2102 'transitionend': function(ev
) {
2103 var node
= ev
.currentTarget
;
2104 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
2105 node
.parentNode
.removeChild(node
);
2108 E('div', { 'style': 'flex:10' }),
2109 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
2112 'style': 'margin-left:auto; margin-top:auto',
2113 'click': function(ev
) {
2114 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
2117 }, [ _('Dismiss') ])
2122 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
2124 L
.dom
.append(msg
.firstElementChild
, children
);
2126 for (var i
= 2; i
< arguments
.length
; i
++)
2127 msg
.classList
.add(arguments
[i
]);
2129 mc
.insertBefore(msg
, mc
.firstElementChild
);
2135 itemlist: function(node
, items
, separators
) {
2138 if (!Array
.isArray(separators
))
2139 separators
= [ separators
|| E('br') ];
2141 for (var i
= 0; i
< items
.length
; i
+= 2) {
2142 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
2143 var sep
= separators
[(i
/2) % separators
.length
],
2146 children
.push(E('span', { class: 'nowrap' }, [
2147 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2151 if ((i
+2) < items
.length
)
2152 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2156 L
.dom
.content(node
, children
);
2162 tabs
: L
.Class
.singleton({
2164 var groups
= [], prevGroup
= null, currGroup
= null;
2166 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2167 var parent
= tab
.parentNode
;
2169 if (L
.dom
.matches(tab
, 'li') && L
.dom
.matches(parent
, 'ul.cbi-tabmenu'))
2172 if (!parent
.hasAttribute('data-tab-group'))
2173 parent
.setAttribute('data-tab-group', groups
.length
);
2175 currGroup
= +parent
.getAttribute('data-tab-group');
2177 if (currGroup
!== prevGroup
) {
2178 prevGroup
= currGroup
;
2180 if (!groups
[currGroup
])
2181 groups
[currGroup
] = [];
2184 groups
[currGroup
].push(tab
);
2187 for (var i
= 0; i
< groups
.length
; i
++)
2188 this.initTabGroup(groups
[i
]);
2190 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2195 initTabGroup: function(panes
) {
2196 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2199 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2200 group
= panes
[0].parentNode
,
2201 groupId
= +group
.getAttribute('data-tab-group'),
2204 if (group
.getAttribute('data-initialized') === 'true')
2207 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2208 var name
= pane
.getAttribute('data-tab'),
2209 title
= pane
.getAttribute('data-tab-title'),
2210 active
= pane
.getAttribute('data-tab-active') === 'true';
2212 menu
.appendChild(E('li', {
2213 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2214 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2218 'click': this.switchTab
.bind(this)
2225 group
.parentNode
.insertBefore(menu
, group
);
2226 group
.setAttribute('data-initialized', true);
2228 if (selected
=== null) {
2229 selected
= this.getActiveTabId(panes
[0]);
2231 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2232 for (var i
= 0; i
< panes
.length
; i
++) {
2233 if (!this.isEmptyPane(panes
[i
])) {
2240 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2241 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2242 panes
[selected
].setAttribute('data-tab-active', 'true');
2244 this.setActiveTabId(panes
[selected
], selected
);
2247 this.updateTabs(group
);
2250 isEmptyPane: function(pane
) {
2251 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2254 getPathForPane: function(pane
) {
2255 var path
= [], node
= null;
2257 for (node
= pane
? pane
.parentNode
: null;
2258 node
!= null && node
.hasAttribute
!= null;
2259 node
= node
.parentNode
)
2261 if (node
.hasAttribute('data-tab'))
2262 path
.unshift(node
.getAttribute('data-tab'));
2263 else if (node
.hasAttribute('data-section-id'))
2264 path
.unshift(node
.getAttribute('data-section-id'));
2267 return path
.join('/');
2270 getActiveTabState: function() {
2271 var page
= document
.body
.getAttribute('data-page');
2274 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2275 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2280 window
.sessionStorage
.removeItem('tab');
2281 return { page
: page
, paths
: {} };
2284 getActiveTabId: function(pane
) {
2285 var path
= this.getPathForPane(pane
);
2286 return +this.getActiveTabState().paths
[path
] || 0;
2289 setActiveTabId: function(pane
, tabIndex
) {
2290 var path
= this.getPathForPane(pane
);
2293 var state
= this.getActiveTabState();
2294 state
.paths
[path
] = tabIndex
;
2296 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2298 catch (e
) { return false; }
2303 updateTabs: function(ev
, root
) {
2304 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2305 var menu
= pane
.parentNode
.previousElementSibling
,
2306 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2307 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2312 if (this.isEmptyPane(pane
)) {
2313 tab
.style
.display
= 'none';
2314 tab
.classList
.remove('flash');
2316 else if (tab
.style
.display
=== 'none') {
2317 tab
.style
.display
= '';
2318 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2322 tab
.setAttribute('data-errors', n_errors
);
2323 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2324 tab
.setAttribute('data-tooltip-style', 'error');
2327 tab
.removeAttribute('data-errors');
2328 tab
.removeAttribute('data-tooltip');
2333 switchTab: function(ev
) {
2334 var tab
= ev
.target
.parentNode
,
2335 name
= tab
.getAttribute('data-tab'),
2336 menu
= tab
.parentNode
,
2337 group
= menu
.nextElementSibling
,
2338 groupId
= +group
.getAttribute('data-tab-group'),
2341 ev
.preventDefault();
2343 if (!tab
.classList
.contains('cbi-tab-disabled'))
2346 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2347 tab
.classList
.remove('cbi-tab');
2348 tab
.classList
.remove('cbi-tab-disabled');
2350 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2353 group
.childNodes
.forEach(function(pane
) {
2354 if (L
.dom
.matches(pane
, '[data-tab]')) {
2355 if (pane
.getAttribute('data-tab') === name
) {
2356 pane
.setAttribute('data-tab-active', 'true');
2357 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2360 pane
.setAttribute('data-tab-active', 'false');
2369 /* File uploading */
2370 uploadFile: function(path
, progressStatusNode
) {
2371 return new Promise(function(resolveFn
, rejectFn
) {
2372 L
.ui
.showModal(_('Uploading file…'), [
2373 E('p', _('Please select the file to upload.')),
2374 E('div', { 'style': 'display:flex' }, [
2375 E('div', { 'class': 'left', 'style': 'flex:1' }, [
2378 style
: 'display:none',
2379 change: function(ev
) {
2380 var modal
= L
.dom
.parent(ev
.target
, '.modal'),
2381 body
= modal
.querySelector('p'),
2382 upload
= modal
.querySelector('.cbi-button-action.important'),
2383 file
= ev
.currentTarget
.files
[0];
2388 L
.dom
.content(body
, [
2390 E('li', {}, [ '%s: %s'.format(_('Name'), file
.name
.replace(/^.*[\\\/]/, '')) ]),
2391 E('li', {}, [ '%s: %1024mB'.format(_('Size'), file
.size
) ])
2395 upload
.disabled
= false;
2401 'click': function(ev
) {
2402 ev
.target
.previousElementSibling
.click();
2404 }, [ _('Browse…') ])
2406 E('div', { 'class': 'right', 'style': 'flex:1' }, [
2409 'click': function() {
2411 rejectFn(new Error('Upload has been cancelled'));
2413 }, [ _('Cancel') ]),
2416 'class': 'btn cbi-button-action important',
2418 'click': function(ev
) {
2419 var input
= L
.dom
.parent(ev
.target
, '.modal').querySelector('input[type="file"]');
2421 if (!input
.files
[0])
2424 var progress
= E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
2426 L
.ui
.showModal(_('Uploading file…'), [ progress
]);
2428 var data
= new FormData();
2430 data
.append('sessionid', rpc
.getSessionID());
2431 data
.append('filename', path
);
2432 data
.append('filedata', input
.files
[0]);
2434 var filename
= input
.files
[0].name
;
2436 L
.Request
.post(L
.env
.cgi_base
+ '/cgi-upload', data
, {
2438 progress: function(pev
) {
2439 var percent
= (pev
.loaded
/ pev
.total
) * 100;
2441 if (progressStatusNode
)
2442 progressStatusNode
.data
= '%.2f%%'.format(percent
);
2444 progress
.setAttribute('title', '%.2f%%'.format(percent
));
2445 progress
.firstElementChild
.style
.width
= '%.2f%%'.format(percent
);
2447 }).then(function(res
) {
2448 var reply
= res
.json();
2452 if (L
.isObject(reply
) && reply
.failure
) {
2453 L
.ui
.addNotification(null, E('p', _('Upload request failed: %s').format(reply
.message
)));
2454 rejectFn(new Error(reply
.failure
));
2457 reply
.name
= filename
;
2472 /* Reconnect handling */
2473 pingDevice: function(proto
, ipaddr
) {
2474 var target
= '%s://%s%s?%s'.format(proto
|| 'http', ipaddr
|| window
.location
.host
, L
.resource('icons/loading.gif'), Math
.random());
2476 return new Promise(function(resolveFn
, rejectFn
) {
2477 var img
= new Image();
2479 img
.onload
= resolveFn
;
2480 img
.onerror
= rejectFn
;
2482 window
.setTimeout(rejectFn
, 1000);
2488 awaitReconnect: function(/* ... */) {
2489 var ipaddrs
= arguments
.length
? arguments
: [ window
.location
.host
];
2491 window
.setTimeout(L
.bind(function() {
2492 L
.Poll
.add(L
.bind(function() {
2493 var tasks
= [], reachable
= false;
2495 for (var i
= 0; i
< 2; i
++)
2496 for (var j
= 0; j
< ipaddrs
.length
; j
++)
2497 tasks
.push(this.pingDevice(i
? 'https' : 'http', ipaddrs
[j
])
2498 .then(function(ev
) { reachable
= ev
.target
.src
.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
2500 return Promise
.all(tasks
).then(function() {
2503 window
.location
= reachable
;
2511 changes
: L
.Class
.singleton({
2513 if (!L
.env
.sessionid
)
2516 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2519 setIndicator: function(n
) {
2520 var i
= document
.querySelector('.uci_change_indicator');
2522 var poll
= document
.getElementById('xhr_poll_status');
2523 i
= poll
.parentNode
.insertBefore(E('a', {
2525 'class': 'uci_change_indicator label notice',
2526 'click': L
.bind(this.displayChanges
, this)
2531 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2532 i
.classList
.add('flash');
2533 i
.style
.display
= '';
2536 i
.classList
.remove('flash');
2537 i
.style
.display
= 'none';
2541 renderChangeIndicator: function(changes
) {
2544 for (var config
in changes
)
2545 if (changes
.hasOwnProperty(config
))
2546 n_changes
+= changes
[config
].length
;
2548 this.changes
= changes
;
2549 this.setIndicator(n_changes
);
2553 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2554 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2555 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2556 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2557 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2558 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2559 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2560 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2561 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2562 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2565 displayChanges: function() {
2566 var list
= E('div', { 'class': 'uci-change-list' }),
2567 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2568 E('div', { 'class': 'cbi-section' }, [
2569 E('strong', _('Legend:')),
2570 E('div', { 'class': 'uci-change-legend' }, [
2571 E('div', { 'class': 'uci-change-legend-label' }, [
2572 E('ins', ' '), ' ', _('Section added') ]),
2573 E('div', { 'class': 'uci-change-legend-label' }, [
2574 E('del', ' '), ' ', _('Section removed') ]),
2575 E('div', { 'class': 'uci-change-legend-label' }, [
2576 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2577 E('div', { 'class': 'uci-change-legend-label' }, [
2578 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2580 E('div', { 'class': 'right' }, [
2583 'click': L
.ui
.hideModal
2584 }, [ _('Dismiss') ]), ' ',
2586 'class': 'cbi-button cbi-button-positive important',
2587 'click': L
.bind(this.apply
, this, true)
2588 }, [ _('Save & Apply') ]), ' ',
2590 'class': 'cbi-button cbi-button-reset',
2591 'click': L
.bind(this.revert
, this)
2592 }, [ _('Revert') ])])])
2595 for (var config
in this.changes
) {
2596 if (!this.changes
.hasOwnProperty(config
))
2599 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2601 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2602 var chg
= this.changes
[config
][i
],
2603 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2605 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2611 if (added
!= null && chg
[1] == added
[0])
2612 return '@' + added
[1] + '[-1]';
2617 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2624 if (chg[0] == 'add')
2625 added = [ chg[1], chg[2] ];
2629 list.appendChild(E('br'));
2630 dlg.classList.add('uci-dialog');
2633 displayStatus: function(type, content) {
2635 var message = L.ui.showModal('', '');
2637 message.classList.add('alert-message');
2638 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2641 L.dom.content(message, content);
2643 if (!this.was_polling) {
2644 this.was_polling = L.Request.poll.active();
2645 L.Request.poll.stop();
2651 if (this.was_polling)
2652 L.Request.poll.start();
2656 rollback: function(checked) {
2658 this.displayStatus('warning spinning',
2659 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2660 .format(L.env.apply_rollback)));
2662 var call = function(r, data, duration) {
2663 if (r.status === 204) {
2664 L.ui.changes.displayStatus('warning', [
2665 E('h4', _('Configuration changes have been rolled back!')),
2666 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)),
2667 E('div', { 'class': 'right' }, [
2670 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2671 }, [ _('Dismiss') ]), ' ',
2673 'class': 'btn cbi-button-action important',
2674 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2675 }, [ _('Revert changes') ]), ' ',
2677 'class': 'btn cbi-button-negative important',
2678 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2679 }, [ _('Apply unchecked') ])
2686 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2687 window.setTimeout(function() {
2688 L.Request.request(L.url('admin/uci/confirm'), {
2690 timeout: L.env.apply_timeout * 1000,
2691 query: { sid: L.env.sessionid, token: L.env.token }
2696 call({ status: 0 });
2699 this.displayStatus('warning', [
2700 E('h4', _('Device unreachable!')),
2701 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.'))
2706 confirm: function(checked, deadline, override_token) {
2708 var ts = Date.now();
2710 this.displayStatus('notice');
2713 this.confirm_auth = { token: override_token };
2715 var call = function(r, data, duration) {
2716 if (Date.now() >= deadline) {
2717 window.clearTimeout(tt);
2718 L.ui.changes.rollback(checked);
2721 else if (r && (r.status === 200 || r.status === 204)) {
2722 document.dispatchEvent(new CustomEvent('uci-applied'));
2724 L.ui.changes.setIndicator(0);
2725 L.ui.changes.displayStatus('notice',
2726 E('p', _('Configuration changes applied.')));
2728 window.clearTimeout(tt);
2729 window.setTimeout(function() {
2730 //L.ui.changes.displayStatus(false);
2731 window.location = window.location.href.split('#')[0];
2732 }, L.env.apply_display * 1000);
2737 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2738 window.setTimeout(function() {
2739 L.Request.request(L.url('admin/uci/confirm'), {
2741 timeout: L.env.apply_timeout * 1000,
2742 query: L.ui.changes.confirm_auth
2743 }).then(call, call);
2747 var tick = function() {
2748 var now = Date.now();
2750 L.ui.changes.displayStatus('notice spinning',
2751 E('p', _('Applying configuration changes… %ds')
2752 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2754 if (now >= deadline)
2757 tt = window.setTimeout(tick, 1000 - (now - ts));
2763 /* wait a few seconds for the settings to become effective */
2764 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2767 apply: function(checked) {
2768 this.displayStatus('notice spinning',
2769 E('p', _('Starting configuration apply…')));
2771 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2773 query: { sid: L.env.sessionid, token: L.env.token }
2774 }).then(function(r) {
2775 if (r.status === (checked ? 200 : 204)) {
2776 var tok = null; try { tok = r.json(); } catch(e) {}
2777 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2778 L.ui.changes.confirm_auth = tok;
2780 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2782 else if (checked && r.status === 204) {
2783 L.ui.changes.displayStatus('notice',
2784 E('p', _('There are no changes to apply')));
2786 window.setTimeout(function() {
2787 L.ui.changes.displayStatus(false);
2788 }, L.env.apply_display * 1000);
2791 L.ui.changes.displayStatus('warning',
2792 E('p', _('Apply request failed with status <code>%h</code>')
2793 .format(r.responseText || r.statusText || r.status)));
2795 window.setTimeout(function() {
2796 L.ui.changes.displayStatus(false);
2797 }, L.env.apply_display * 1000);
2802 revert: function() {
2803 this.displayStatus('notice spinning',
2804 E('p', _('Reverting configuration…')));
2806 L.Request.request(L.url('admin/uci/revert'), {
2808 query: { sid: L.env.sessionid, token: L.env.token }
2809 }).then(function(r) {
2810 if (r.status === 200) {
2811 document.dispatchEvent(new CustomEvent('uci-reverted'));
2813 L.ui.changes.setIndicator(0);
2814 L.ui.changes.displayStatus('notice',
2815 E('p', _('Changes have been reverted.')));
2817 window.setTimeout(function() {
2818 //L.ui.changes.displayStatus(false);
2819 window.location = window.location.href.split('#')[0];
2820 }, L.env.apply_display * 1000);
2823 L.ui.changes.displayStatus('warning',
2824 E('p', _('Revert request failed with status <code>%h</code>')
2825 .format(r.statusText || r.status)));
2827 window.setTimeout(function() {
2828 L.ui.changes.displayStatus(false);
2829 }, L.env.apply_display * 1000);
2835 addValidator: function(field, type, optional, vfunc /*, ... */) {
2839 var events = this.varargs(arguments, 3);
2840 if (events.length == 0)
2841 events.push('blur', 'keyup');
2844 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2845 validatorFn = cbiValidator.validate.bind(cbiValidator);
2847 for (var i = 0; i < events.length; i++)
2848 field.addEventListener(events[i], validatorFn);
2857 createHandlerFn: function(ctx, fn /*, ... */) {
2858 if (typeof(fn) == 'string')
2861 if (typeof(fn) != 'function')
2864 var arg_offset = arguments.length - 2;
2866 return Function.prototype.bind.apply(function() {
2867 var t = arguments[arg_offset].target;
2869 t.classList.add('spinning');
2875 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2876 t.classList.remove('spinning');
2879 }, this.varargs(arguments, 2, ctx));
2882 AbstractElement: UIElement,
2885 Textfield: UITextfield,
2886 Textarea: UITextarea,
2887 Checkbox: UICheckbox,
2889 Dropdown: UIDropdown,
2890 DynamicList: UIDynamicList,
2891 Combobox: UICombobox,
2892 ComboButton: UIComboButton,
2893 Hiddenfield: UIHiddenfield,
2894 FileUpload: UIFileUpload