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', {}));
1924 handleReset: function(ev
) {
1925 var button
= this.node
.firstElementChild
,
1926 hidden
= this.node
.lastElementChild
;
1929 L
.dom
.content(button
, _('Select file…'));
1931 this.handleCancel(ev
);
1934 handleSelect: function(path
, fileStat
, ev
) {
1935 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1936 ul
= browser
.querySelector('ul');
1938 if (fileStat
== null) {
1939 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1940 L
.resolveDefault(fs
.list(path
), []).then(L
.bind(this.renderListing
, this, browser
, path
));
1943 var button
= this.node
.firstElementChild
,
1944 hidden
= this.node
.lastElementChild
;
1946 path
= this.canonicalizePath(path
);
1948 L
.dom
.content(button
, [
1949 this.iconForType(fileStat
.type
),
1950 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1953 browser
.classList
.remove('open');
1954 button
.style
.display
= '';
1955 hidden
.value
= path
;
1957 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1958 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1962 handleFileBrowser: function(ev
) {
1963 var button
= ev
.target
,
1964 browser
= button
.nextElementSibling
,
1965 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : this.options
.root_directory
;
1967 if (this.options
.root_directory
.indexOf(path
) != 0)
1968 path
= this.options
.root_directory
;
1970 ev
.preventDefault();
1972 return L
.resolveDefault(fs
.list(path
), []).then(L
.bind(function(button
, browser
, path
, list
) {
1973 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1974 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1977 button
.style
.display
= 'none';
1978 browser
.classList
.add('open');
1980 return this.renderListing(browser
, path
, list
);
1981 }, this, button
, browser
, path
));
1984 getValue: function() {
1985 return this.node
.lastElementChild
.value
;
1988 setValue: function(value
) {
1989 this.node
.lastElementChild
.value
= value
;
1994 return L
.Class
.extend({
1995 __init__: function() {
1996 modalDiv
= document
.body
.appendChild(
1997 L
.dom
.create('div', { id
: 'modal_overlay' },
1998 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
2000 tooltipDiv
= document
.body
.appendChild(
2001 L
.dom
.create('div', { class: 'cbi-tooltip' }));
2003 /* setup old aliases */
2004 L
.showModal
= this.showModal
;
2005 L
.hideModal
= this.hideModal
;
2006 L
.showTooltip
= this.showTooltip
;
2007 L
.hideTooltip
= this.hideTooltip
;
2008 L
.itemlist
= this.itemlist
;
2010 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
2011 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
2012 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
2013 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
2015 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
2016 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
2017 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
2021 showModal: function(title
, children
/* , ... */) {
2022 var dlg
= modalDiv
.firstElementChild
;
2024 dlg
.setAttribute('class', 'modal');
2026 for (var i
= 2; i
< arguments
.length
; i
++)
2027 dlg
.classList
.add(arguments
[i
]);
2029 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
2030 L
.dom
.append(dlg
, children
);
2032 document
.body
.classList
.add('modal-overlay-active');
2037 hideModal: function() {
2038 document
.body
.classList
.remove('modal-overlay-active');
2042 showTooltip: function(ev
) {
2043 var target
= findParent(ev
.target
, '[data-tooltip]');
2048 if (tooltipTimeout
!== null) {
2049 window
.clearTimeout(tooltipTimeout
);
2050 tooltipTimeout
= null;
2053 var rect
= target
.getBoundingClientRect(),
2054 x
= rect
.left
+ window
.pageXOffset
,
2055 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
2057 tooltipDiv
.className
= 'cbi-tooltip';
2058 tooltipDiv
.innerHTML
= '▲ ';
2059 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
2061 if (target
.hasAttribute('data-tooltip-style'))
2062 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
2064 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
2065 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
2066 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
2069 tooltipDiv
.style
.top
= y
+ 'px';
2070 tooltipDiv
.style
.left
= x
+ 'px';
2071 tooltipDiv
.style
.opacity
= 1;
2073 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
2075 detail
: { target
: target
}
2079 hideTooltip: function(ev
) {
2080 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
2081 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
2084 if (tooltipTimeout
!== null) {
2085 window
.clearTimeout(tooltipTimeout
);
2086 tooltipTimeout
= null;
2089 tooltipDiv
.style
.opacity
= 0;
2090 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
2092 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
2095 addNotification: function(title
, children
/*, ... */) {
2096 var mc
= document
.querySelector('#maincontent') || document
.body
;
2097 var msg
= E('div', {
2098 'class': 'alert-message fade-in',
2099 'style': 'display:flex',
2100 'transitionend': function(ev
) {
2101 var node
= ev
.currentTarget
;
2102 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
2103 node
.parentNode
.removeChild(node
);
2106 E('div', { 'style': 'flex:10' }),
2107 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
2110 'style': 'margin-left:auto; margin-top:auto',
2111 'click': function(ev
) {
2112 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
2115 }, [ _('Dismiss') ])
2120 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
2122 L
.dom
.append(msg
.firstElementChild
, children
);
2124 for (var i
= 2; i
< arguments
.length
; i
++)
2125 msg
.classList
.add(arguments
[i
]);
2127 mc
.insertBefore(msg
, mc
.firstElementChild
);
2133 itemlist: function(node
, items
, separators
) {
2136 if (!Array
.isArray(separators
))
2137 separators
= [ separators
|| E('br') ];
2139 for (var i
= 0; i
< items
.length
; i
+= 2) {
2140 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
2141 var sep
= separators
[(i
/2) % separators
.length
],
2144 children
.push(E('span', { class: 'nowrap' }, [
2145 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2149 if ((i
+2) < items
.length
)
2150 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2154 L
.dom
.content(node
, children
);
2160 tabs
: L
.Class
.singleton({
2162 var groups
= [], prevGroup
= null, currGroup
= null;
2164 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2165 var parent
= tab
.parentNode
;
2167 if (L
.dom
.matches(tab
, 'li') && L
.dom
.matches(parent
, 'ul.cbi-tabmenu'))
2170 if (!parent
.hasAttribute('data-tab-group'))
2171 parent
.setAttribute('data-tab-group', groups
.length
);
2173 currGroup
= +parent
.getAttribute('data-tab-group');
2175 if (currGroup
!== prevGroup
) {
2176 prevGroup
= currGroup
;
2178 if (!groups
[currGroup
])
2179 groups
[currGroup
] = [];
2182 groups
[currGroup
].push(tab
);
2185 for (var i
= 0; i
< groups
.length
; i
++)
2186 this.initTabGroup(groups
[i
]);
2188 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2193 initTabGroup: function(panes
) {
2194 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2197 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2198 group
= panes
[0].parentNode
,
2199 groupId
= +group
.getAttribute('data-tab-group'),
2202 if (group
.getAttribute('data-initialized') === 'true')
2205 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2206 var name
= pane
.getAttribute('data-tab'),
2207 title
= pane
.getAttribute('data-tab-title'),
2208 active
= pane
.getAttribute('data-tab-active') === 'true';
2210 menu
.appendChild(E('li', {
2211 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2212 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2216 'click': this.switchTab
.bind(this)
2223 group
.parentNode
.insertBefore(menu
, group
);
2224 group
.setAttribute('data-initialized', true);
2226 if (selected
=== null) {
2227 selected
= this.getActiveTabId(panes
[0]);
2229 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2230 for (var i
= 0; i
< panes
.length
; i
++) {
2231 if (!this.isEmptyPane(panes
[i
])) {
2238 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2239 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2240 panes
[selected
].setAttribute('data-tab-active', 'true');
2242 this.setActiveTabId(panes
[selected
], selected
);
2245 this.updateTabs(group
);
2248 isEmptyPane: function(pane
) {
2249 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2252 getPathForPane: function(pane
) {
2253 var path
= [], node
= null;
2255 for (node
= pane
? pane
.parentNode
: null;
2256 node
!= null && node
.hasAttribute
!= null;
2257 node
= node
.parentNode
)
2259 if (node
.hasAttribute('data-tab'))
2260 path
.unshift(node
.getAttribute('data-tab'));
2261 else if (node
.hasAttribute('data-section-id'))
2262 path
.unshift(node
.getAttribute('data-section-id'));
2265 return path
.join('/');
2268 getActiveTabState: function() {
2269 var page
= document
.body
.getAttribute('data-page');
2272 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2273 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2278 window
.sessionStorage
.removeItem('tab');
2279 return { page
: page
, paths
: {} };
2282 getActiveTabId: function(pane
) {
2283 var path
= this.getPathForPane(pane
);
2284 return +this.getActiveTabState().paths
[path
] || 0;
2287 setActiveTabId: function(pane
, tabIndex
) {
2288 var path
= this.getPathForPane(pane
);
2291 var state
= this.getActiveTabState();
2292 state
.paths
[path
] = tabIndex
;
2294 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2296 catch (e
) { return false; }
2301 updateTabs: function(ev
, root
) {
2302 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2303 var menu
= pane
.parentNode
.previousElementSibling
,
2304 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2305 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2310 if (this.isEmptyPane(pane
)) {
2311 tab
.style
.display
= 'none';
2312 tab
.classList
.remove('flash');
2314 else if (tab
.style
.display
=== 'none') {
2315 tab
.style
.display
= '';
2316 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2320 tab
.setAttribute('data-errors', n_errors
);
2321 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2322 tab
.setAttribute('data-tooltip-style', 'error');
2325 tab
.removeAttribute('data-errors');
2326 tab
.removeAttribute('data-tooltip');
2331 switchTab: function(ev
) {
2332 var tab
= ev
.target
.parentNode
,
2333 name
= tab
.getAttribute('data-tab'),
2334 menu
= tab
.parentNode
,
2335 group
= menu
.nextElementSibling
,
2336 groupId
= +group
.getAttribute('data-tab-group'),
2339 ev
.preventDefault();
2341 if (!tab
.classList
.contains('cbi-tab-disabled'))
2344 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2345 tab
.classList
.remove('cbi-tab');
2346 tab
.classList
.remove('cbi-tab-disabled');
2348 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2351 group
.childNodes
.forEach(function(pane
) {
2352 if (L
.dom
.matches(pane
, '[data-tab]')) {
2353 if (pane
.getAttribute('data-tab') === name
) {
2354 pane
.setAttribute('data-tab-active', 'true');
2355 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2358 pane
.setAttribute('data-tab-active', 'false');
2367 /* File uploading */
2368 uploadFile: function(path
, progressStatusNode
) {
2369 return new Promise(function(resolveFn
, rejectFn
) {
2370 L
.ui
.showModal(_('Uploading file…'), [
2371 E('p', _('Please select the file to upload.')),
2372 E('div', { 'style': 'display:flex' }, [
2373 E('div', { 'class': 'left', 'style': 'flex:1' }, [
2376 style
: 'display:none',
2377 change: function(ev
) {
2378 var modal
= L
.dom
.parent(ev
.target
, '.modal'),
2379 body
= modal
.querySelector('p'),
2380 upload
= modal
.querySelector('.cbi-button-action.important'),
2381 file
= ev
.currentTarget
.files
[0];
2386 L
.dom
.content(body
, [
2388 E('li', {}, [ '%s: %s'.format(_('Name'), file
.name
.replace(/^.*[\\\/]/, '')) ]),
2389 E('li', {}, [ '%s: %1024mB'.format(_('Size'), file
.size
) ])
2393 upload
.disabled
= false;
2399 'click': function(ev
) {
2400 ev
.target
.previousElementSibling
.click();
2402 }, [ _('Browse…') ])
2404 E('div', { 'class': 'right', 'style': 'flex:1' }, [
2407 'click': function() {
2409 rejectFn(new Error('Upload has been cancelled'));
2411 }, [ _('Cancel') ]),
2414 'class': 'btn cbi-button-action important',
2416 'click': function(ev
) {
2417 var input
= L
.dom
.parent(ev
.target
, '.modal').querySelector('input[type="file"]');
2419 if (!input
.files
[0])
2422 var progress
= E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
2424 L
.ui
.showModal(_('Uploading file…'), [ progress
]);
2426 var data
= new FormData();
2428 data
.append('sessionid', rpc
.getSessionID());
2429 data
.append('filename', path
);
2430 data
.append('filedata', input
.files
[0]);
2432 var filename
= input
.files
[0].name
;
2434 L
.Request
.post(L
.env
.cgi_base
+ '/cgi-upload', data
, {
2436 progress: function(pev
) {
2437 var percent
= (pev
.loaded
/ pev
.total
) * 100;
2439 if (progressStatusNode
)
2440 progressStatusNode
.data
= '%.2f%%'.format(percent
);
2442 progress
.setAttribute('title', '%.2f%%'.format(percent
));
2443 progress
.firstElementChild
.style
.width
= '%.2f%%'.format(percent
);
2445 }).then(function(res
) {
2446 var reply
= res
.json();
2450 if (L
.isObject(reply
) && reply
.failure
) {
2451 L
.ui
.addNotification(null, E('p', _('Upload request failed: %s').format(reply
.message
)));
2452 rejectFn(new Error(reply
.failure
));
2455 reply
.name
= filename
;
2470 /* Reconnect handling */
2471 pingDevice: function(proto
, ipaddr
) {
2472 var target
= '%s://%s%s?%s'.format(proto
|| 'http', ipaddr
|| window
.location
.host
, L
.resource('icons/loading.gif'), Math
.random());
2474 return new Promise(function(resolveFn
, rejectFn
) {
2475 var img
= new Image();
2477 img
.onload
= resolveFn
;
2478 img
.onerror
= rejectFn
;
2480 window
.setTimeout(rejectFn
, 1000);
2486 awaitReconnect: function(/* ... */) {
2487 var ipaddrs
= arguments
.length
? arguments
: [ window
.location
.host
];
2489 window
.setTimeout(L
.bind(function() {
2490 L
.Poll
.add(L
.bind(function() {
2491 var tasks
= [], reachable
= false;
2493 for (var i
= 0; i
< 2; i
++)
2494 for (var j
= 0; j
< ipaddrs
.length
; j
++)
2495 tasks
.push(this.pingDevice(i
? 'https' : 'http', ipaddrs
[j
])
2496 .then(function(ev
) { reachable
= ev
.target
.src
.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
2498 return Promise
.all(tasks
).then(function() {
2501 window
.location
= reachable
;
2509 changes
: L
.Class
.singleton({
2511 if (!L
.env
.sessionid
)
2514 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2517 setIndicator: function(n
) {
2518 var i
= document
.querySelector('.uci_change_indicator');
2520 var poll
= document
.getElementById('xhr_poll_status');
2521 i
= poll
.parentNode
.insertBefore(E('a', {
2523 'class': 'uci_change_indicator label notice',
2524 'click': L
.bind(this.displayChanges
, this)
2529 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2530 i
.classList
.add('flash');
2531 i
.style
.display
= '';
2534 i
.classList
.remove('flash');
2535 i
.style
.display
= 'none';
2539 renderChangeIndicator: function(changes
) {
2542 for (var config
in changes
)
2543 if (changes
.hasOwnProperty(config
))
2544 n_changes
+= changes
[config
].length
;
2546 this.changes
= changes
;
2547 this.setIndicator(n_changes
);
2551 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2552 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2553 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2554 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2555 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2556 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2557 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2558 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2559 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2560 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2563 displayChanges: function() {
2564 var list
= E('div', { 'class': 'uci-change-list' }),
2565 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2566 E('div', { 'class': 'cbi-section' }, [
2567 E('strong', _('Legend:')),
2568 E('div', { 'class': 'uci-change-legend' }, [
2569 E('div', { 'class': 'uci-change-legend-label' }, [
2570 E('ins', ' '), ' ', _('Section added') ]),
2571 E('div', { 'class': 'uci-change-legend-label' }, [
2572 E('del', ' '), ' ', _('Section removed') ]),
2573 E('div', { 'class': 'uci-change-legend-label' }, [
2574 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2575 E('div', { 'class': 'uci-change-legend-label' }, [
2576 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2578 E('div', { 'class': 'right' }, [
2581 'click': L
.ui
.hideModal
2582 }, [ _('Dismiss') ]), ' ',
2584 'class': 'cbi-button cbi-button-positive important',
2585 'click': L
.bind(this.apply
, this, true)
2586 }, [ _('Save & Apply') ]), ' ',
2588 'class': 'cbi-button cbi-button-reset',
2589 'click': L
.bind(this.revert
, this)
2590 }, [ _('Revert') ])])])
2593 for (var config
in this.changes
) {
2594 if (!this.changes
.hasOwnProperty(config
))
2597 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2599 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2600 var chg
= this.changes
[config
][i
],
2601 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2603 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2609 if (added
!= null && chg
[1] == added
[0])
2610 return '@' + added
[1] + '[-1]';
2615 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2622 if (chg[0] == 'add')
2623 added = [ chg[1], chg[2] ];
2627 list.appendChild(E('br'));
2628 dlg.classList.add('uci-dialog');
2631 displayStatus: function(type, content) {
2633 var message = L.ui.showModal('', '');
2635 message.classList.add('alert-message');
2636 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2639 L.dom.content(message, content);
2641 if (!this.was_polling) {
2642 this.was_polling = L.Request.poll.active();
2643 L.Request.poll.stop();
2649 if (this.was_polling)
2650 L.Request.poll.start();
2654 rollback: function(checked) {
2656 this.displayStatus('warning spinning',
2657 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2658 .format(L.env.apply_rollback)));
2660 var call = function(r, data, duration) {
2661 if (r.status === 204) {
2662 L.ui.changes.displayStatus('warning', [
2663 E('h4', _('Configuration changes have been rolled back!')),
2664 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)),
2665 E('div', { 'class': 'right' }, [
2668 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2669 }, [ _('Dismiss') ]), ' ',
2671 'class': 'btn cbi-button-action important',
2672 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2673 }, [ _('Revert changes') ]), ' ',
2675 'class': 'btn cbi-button-negative important',
2676 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2677 }, [ _('Apply unchecked') ])
2684 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2685 window.setTimeout(function() {
2686 L.Request.request(L.url('admin/uci/confirm'), {
2688 timeout: L.env.apply_timeout * 1000,
2689 query: { sid: L.env.sessionid, token: L.env.token }
2694 call({ status: 0 });
2697 this.displayStatus('warning', [
2698 E('h4', _('Device unreachable!')),
2699 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.'))
2704 confirm: function(checked, deadline, override_token) {
2706 var ts = Date.now();
2708 this.displayStatus('notice');
2711 this.confirm_auth = { token: override_token };
2713 var call = function(r, data, duration) {
2714 if (Date.now() >= deadline) {
2715 window.clearTimeout(tt);
2716 L.ui.changes.rollback(checked);
2719 else if (r && (r.status === 200 || r.status === 204)) {
2720 document.dispatchEvent(new CustomEvent('uci-applied'));
2722 L.ui.changes.setIndicator(0);
2723 L.ui.changes.displayStatus('notice',
2724 E('p', _('Configuration changes applied.')));
2726 window.clearTimeout(tt);
2727 window.setTimeout(function() {
2728 //L.ui.changes.displayStatus(false);
2729 window.location = window.location.href.split('#')[0];
2730 }, L.env.apply_display * 1000);
2735 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2736 window.setTimeout(function() {
2737 L.Request.request(L.url('admin/uci/confirm'), {
2739 timeout: L.env.apply_timeout * 1000,
2740 query: L.ui.changes.confirm_auth
2741 }).then(call, call);
2745 var tick = function() {
2746 var now = Date.now();
2748 L.ui.changes.displayStatus('notice spinning',
2749 E('p', _('Applying configuration changes… %ds')
2750 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2752 if (now >= deadline)
2755 tt = window.setTimeout(tick, 1000 - (now - ts));
2761 /* wait a few seconds for the settings to become effective */
2762 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2765 apply: function(checked) {
2766 this.displayStatus('notice spinning',
2767 E('p', _('Starting configuration apply…')));
2769 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2771 query: { sid: L.env.sessionid, token: L.env.token }
2772 }).then(function(r) {
2773 if (r.status === (checked ? 200 : 204)) {
2774 var tok = null; try { tok = r.json(); } catch(e) {}
2775 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2776 L.ui.changes.confirm_auth = tok;
2778 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2780 else if (checked && r.status === 204) {
2781 L.ui.changes.displayStatus('notice',
2782 E('p', _('There are no changes to apply')));
2784 window.setTimeout(function() {
2785 L.ui.changes.displayStatus(false);
2786 }, L.env.apply_display * 1000);
2789 L.ui.changes.displayStatus('warning',
2790 E('p', _('Apply request failed with status <code>%h</code>')
2791 .format(r.responseText || r.statusText || r.status)));
2793 window.setTimeout(function() {
2794 L.ui.changes.displayStatus(false);
2795 }, L.env.apply_display * 1000);
2800 revert: function() {
2801 this.displayStatus('notice spinning',
2802 E('p', _('Reverting configuration…')));
2804 L.Request.request(L.url('admin/uci/revert'), {
2806 query: { sid: L.env.sessionid, token: L.env.token }
2807 }).then(function(r) {
2808 if (r.status === 200) {
2809 document.dispatchEvent(new CustomEvent('uci-reverted'));
2811 L.ui.changes.setIndicator(0);
2812 L.ui.changes.displayStatus('notice',
2813 E('p', _('Changes have been reverted.')));
2815 window.setTimeout(function() {
2816 //L.ui.changes.displayStatus(false);
2817 window.location = window.location.href.split('#')[0];
2818 }, L.env.apply_display * 1000);
2821 L.ui.changes.displayStatus('warning',
2822 E('p', _('Revert request failed with status <code>%h</code>')
2823 .format(r.statusText || r.status)));
2825 window.setTimeout(function() {
2826 L.ui.changes.displayStatus(false);
2827 }, L.env.apply_display * 1000);
2833 addValidator: function(field, type, optional, vfunc /*, ... */) {
2837 var events = this.varargs(arguments, 3);
2838 if (events.length == 0)
2839 events.push('blur', 'keyup');
2842 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2843 validatorFn = cbiValidator.validate.bind(cbiValidator);
2845 for (var i = 0; i < events.length; i++)
2846 field.addEventListener(events[i], validatorFn);
2855 createHandlerFn: function(ctx, fn /*, ... */) {
2856 if (typeof(fn) == 'string')
2859 if (typeof(fn) != 'function')
2862 var arg_offset = arguments.length - 2;
2864 return Function.prototype.bind.apply(function() {
2865 var t = arguments[arg_offset].target;
2867 t.classList.add('spinning');
2873 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2874 t.classList.remove('spinning');
2877 }, this.varargs(arguments, 2, ctx));
2880 AbstractElement: UIElement,
2883 Textfield: UITextfield,
2884 Textarea: UITextarea,
2885 Checkbox: UICheckbox,
2887 Dropdown: UIDropdown,
2888 DynamicList: UIDynamicList,
2889 Combobox: UICombobox,
2890 ComboButton: UIComboButton,
2891 Hiddenfield: UIHiddenfield,
2892 FileUpload: UIFileUpload