11 var UIElement
= L
.Class
.extend({
12 getValue: function() {
13 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
14 return this.node
.value
;
19 setValue: function(value
) {
20 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
21 this.node
.value
= value
;
25 return (this.validState
!== false);
28 triggerValidation: function() {
29 if (typeof(this.vfunc
) != 'function')
32 var wasValid
= this.isValid();
36 return (wasValid
!= this.isValid());
39 registerEvents: function(targetNode
, synevent
, events
) {
40 var dispatchFn
= L
.bind(function(ev
) {
41 this.node
.dispatchEvent(new CustomEvent(synevent
, { bubbles
: true }));
44 for (var i
= 0; i
< events
.length
; i
++)
45 targetNode
.addEventListener(events
[i
], dispatchFn
);
48 setUpdateEvents: function(targetNode
/*, ... */) {
49 var datatype
= this.options
.datatype
,
50 optional
= this.options
.hasOwnProperty('optional') ? this.options
.optional
: true,
51 validate
= this.options
.validate
,
52 events
= this.varargs(arguments
, 1);
54 this.registerEvents(targetNode
, 'widget-update', events
);
56 if (!datatype
&& !validate
)
59 this.vfunc
= L
.ui
.addValidator
.apply(L
.ui
, [
60 targetNode
, datatype
|| 'string',
64 this.node
.addEventListener('validation-success', L
.bind(function(ev
) {
65 this.validState
= true;
68 this.node
.addEventListener('validation-failure', L
.bind(function(ev
) {
69 this.validState
= false;
73 setChangeEvents: function(targetNode
/*, ... */) {
74 var tag_changed
= L
.bind(function(ev
) { this.setAttribute('data-changed', true) }, this.node
);
76 for (var i
= 1; i
< arguments
.length
; i
++)
77 targetNode
.addEventListener(arguments
[i
], tag_changed
);
79 this.registerEvents(targetNode
, 'widget-change', this.varargs(arguments
, 1));
83 var UITextfield
= UIElement
.extend({
84 __init__: function(value
, options
) {
86 this.options
= Object
.assign({
93 var frameEl
= E('div', { 'id': this.options
.id
});
95 if (this.options
.password
) {
96 frameEl
.classList
.add('nowrap');
97 frameEl
.appendChild(E('input', {
99 'style': 'position:absolute; left:-100000px',
102 'name': this.options
.name
? 'password.%s'.format(this.options
.name
) : null
106 frameEl
.appendChild(E('input', {
107 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
108 'name': this.options
.name
,
109 'type': this.options
.password
? 'password' : 'text',
110 'class': this.options
.password
? 'cbi-input-password' : 'cbi-input-text',
111 'readonly': this.options
.readonly
? '' : null,
112 'maxlength': this.options
.maxlength
,
113 'placeholder': this.options
.placeholder
,
117 if (this.options
.password
)
118 frameEl
.appendChild(E('button', {
119 'class': 'cbi-button cbi-button-neutral',
120 'title': _('Reveal/hide password'),
121 'aria-label': _('Reveal/hide password'),
122 'click': function(ev
) {
123 var e
= this.previousElementSibling
;
124 e
.type
= (e
.type
=== 'password') ? 'text' : 'password';
129 return this.bind(frameEl
);
132 bind: function(frameEl
) {
133 var inputEl
= frameEl
.childNodes
[+!!this.options
.password
];
137 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
138 this.setChangeEvents(inputEl
, 'change');
140 L
.dom
.bindClassInstance(frameEl
, this);
145 getValue: function() {
146 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
147 return inputEl
.value
;
150 setValue: function(value
) {
151 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
152 inputEl
.value
= value
;
156 var UITextarea
= UIElement
.extend({
157 __init__: function(value
, options
) {
159 this.options
= Object
.assign({
168 var frameEl
= E('div', { 'id': this.options
.id
}),
169 value
= (this.value
!= null) ? String(this.value
) : '';
171 frameEl
.appendChild(E('textarea', {
172 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
173 'name': this.options
.name
,
174 'class': 'cbi-input-textarea',
175 'readonly': this.options
.readonly
? '' : null,
176 'placeholder': this.options
.placeholder
,
177 'style': !this.options
.cols
? 'width:100%' : null,
178 'cols': this.options
.cols
,
179 'rows': this.options
.rows
,
180 'wrap': this.options
.wrap
? '' : null
183 if (this.options
.monospace
)
184 frameEl
.firstElementChild
.style
.fontFamily
= 'monospace';
186 return this.bind(frameEl
);
189 bind: function(frameEl
) {
190 var inputEl
= frameEl
.firstElementChild
;
194 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
195 this.setChangeEvents(inputEl
, 'change');
197 L
.dom
.bindClassInstance(frameEl
, this);
202 getValue: function() {
203 return this.node
.firstElementChild
.value
;
206 setValue: function(value
) {
207 this.node
.firstElementChild
.value
= value
;
211 var UICheckbox
= UIElement
.extend({
212 __init__: function(value
, options
) {
214 this.options
= Object
.assign({
221 var frameEl
= E('div', {
222 'id': this.options
.id
,
223 'class': 'cbi-checkbox'
226 if (this.options
.hiddenname
)
227 frameEl
.appendChild(E('input', {
229 'name': this.options
.hiddenname
,
233 frameEl
.appendChild(E('input', {
234 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
235 'name': this.options
.name
,
237 'value': this.options
.value_enabled
,
238 'checked': (this.value
== this.options
.value_enabled
) ? '' : null
241 return this.bind(frameEl
);
244 bind: function(frameEl
) {
247 this.setUpdateEvents(frameEl
.lastElementChild
, 'click', 'blur');
248 this.setChangeEvents(frameEl
.lastElementChild
, 'change');
250 L
.dom
.bindClassInstance(frameEl
, this);
255 isChecked: function() {
256 return this.node
.lastElementChild
.checked
;
259 getValue: function() {
260 return this.isChecked()
261 ? this.options
.value_enabled
262 : this.options
.value_disabled
;
265 setValue: function(value
) {
266 this.node
.lastElementChild
.checked
= (value
== this.options
.value_enabled
);
270 var UISelect
= UIElement
.extend({
271 __init__: function(value
, choices
, options
) {
272 if (!L
.isObject(choices
))
275 if (!Array
.isArray(value
))
276 value
= (value
!= null && value
!= '') ? [ value
] : [];
278 if (!options
.multiple
&& value
.length
> 1)
282 this.choices
= choices
;
283 this.options
= Object
.assign({
286 orientation
: 'horizontal'
289 if (this.choices
.hasOwnProperty(''))
290 this.options
.optional
= true;
294 var frameEl
= E('div', { 'id': this.options
.id
}),
295 keys
= Object
.keys(this.choices
);
297 if (this.options
.sort
=== true)
299 else if (Array
.isArray(this.options
.sort
))
300 keys
= this.options
.sort
;
302 if (this.options
.widget
== 'select') {
303 frameEl
.appendChild(E('select', {
304 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
305 'name': this.options
.name
,
306 'size': this.options
.size
,
307 'class': 'cbi-input-select',
308 'multiple': this.options
.multiple
? '' : null
311 if (this.options
.optional
)
312 frameEl
.lastChild
.appendChild(E('option', {
314 'selected': (this.values
.length
== 0 || this.values
[0] == '') ? '' : null
315 }, [ this.choices
[''] || this.options
.placeholder
|| _('-- Please choose --') ]));
317 for (var i
= 0; i
< keys
.length
; i
++) {
318 if (keys
[i
] == null || keys
[i
] == '')
321 frameEl
.lastChild
.appendChild(E('option', {
323 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
324 }, [ this.choices
[keys
[i
]] || keys
[i
] ]));
328 var brEl
= (this.options
.orientation
=== 'horizontal') ? document
.createTextNode(' ') : E('br');
330 for (var i
= 0; i
< keys
.length
; i
++) {
331 frameEl
.appendChild(E('label', {}, [
333 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
334 'name': this.options
.id
|| this.options
.name
,
335 'type': this.options
.multiple
? 'checkbox' : 'radio',
336 'class': this.options
.multiple
? 'cbi-input-checkbox' : 'cbi-input-radio',
338 'checked': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
340 this.choices
[keys
[i
]] || keys
[i
]
343 if (i
+ 1 == this.options
.size
)
344 frameEl
.appendChild(brEl
);
348 return this.bind(frameEl
);
351 bind: function(frameEl
) {
354 if (this.options
.widget
== 'select') {
355 this.setUpdateEvents(frameEl
.firstChild
, 'change', 'click', 'blur');
356 this.setChangeEvents(frameEl
.firstChild
, 'change');
359 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
360 for (var i
= 0; i
< radioEls
.length
; i
++) {
361 this.setUpdateEvents(radioEls
[i
], 'change', 'click', 'blur');
362 this.setChangeEvents(radioEls
[i
], 'change', 'click', 'blur');
366 L
.dom
.bindClassInstance(frameEl
, this);
371 getValue: function() {
372 if (this.options
.widget
== 'select')
373 return this.node
.firstChild
.value
;
375 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
376 for (var i
= 0; i
< radioEls
.length
; i
++)
377 if (radioEls
[i
].checked
)
378 return radioEls
[i
].value
;
383 setValue: function(value
) {
384 if (this.options
.widget
== 'select') {
388 for (var i
= 0; i
< this.node
.firstChild
.options
.length
; i
++)
389 this.node
.firstChild
.options
[i
].selected
= (this.node
.firstChild
.options
[i
].value
== value
);
394 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
395 for (var i
= 0; i
< radioEls
.length
; i
++)
396 radioEls
[i
].checked
= (radioEls
[i
].value
== value
);
400 var UIDropdown
= UIElement
.extend({
401 __init__: function(value
, choices
, options
) {
402 if (typeof(choices
) != 'object')
405 if (!Array
.isArray(value
))
406 this.values
= (value
!= null && value
!= '') ? [ value
] : [];
410 this.choices
= choices
;
411 this.options
= Object
.assign({
413 multiple
: Array
.isArray(value
),
415 select_placeholder
: _('-- Please choose --'),
416 custom_placeholder
: _('-- custom --'),
420 create_query
: '.create-item-input',
421 create_template
: 'script[type="item-template"]'
427 'id': this.options
.id
,
428 'class': 'cbi-dropdown',
429 'multiple': this.options
.multiple
? '' : null,
430 'optional': this.options
.optional
? '' : null,
433 var keys
= Object
.keys(this.choices
);
435 if (this.options
.sort
=== true)
437 else if (Array
.isArray(this.options
.sort
))
438 keys
= this.options
.sort
;
440 if (this.options
.create
)
441 for (var i
= 0; i
< this.values
.length
; i
++)
442 if (!this.choices
.hasOwnProperty(this.values
[i
]))
443 keys
.push(this.values
[i
]);
445 for (var i
= 0; i
< keys
.length
; i
++)
446 sb
.lastElementChild
.appendChild(E('li', {
447 'data-value': keys
[i
],
448 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
449 }, this.choices
[keys
[i
]] || keys
[i
]));
451 if (this.options
.create
) {
452 var createEl
= E('input', {
454 'class': 'create-item-input',
455 'readonly': this.options
.readonly
? '' : null,
456 'maxlength': this.options
.maxlength
,
457 'placeholder': this.options
.custom_placeholder
|| this.options
.placeholder
460 if (this.options
.datatype
|| this.options
.validate
)
461 L
.ui
.addValidator(createEl
, this.options
.datatype
|| 'string',
462 true, this.options
.validate
, 'blur', 'keyup');
464 sb
.lastElementChild
.appendChild(E('li', { 'data-value': '-' }, createEl
));
467 if (this.options
.create_markup
)
468 sb
.appendChild(E('script', { type
: 'item-template' },
469 this.options
.create_markup
));
471 return this.bind(sb
);
475 var o
= this.options
;
477 o
.multiple
= sb
.hasAttribute('multiple');
478 o
.optional
= sb
.hasAttribute('optional');
479 o
.placeholder
= sb
.getAttribute('placeholder') || o
.placeholder
;
480 o
.display_items
= parseInt(sb
.getAttribute('display-items') || o
.display_items
);
481 o
.dropdown_items
= parseInt(sb
.getAttribute('dropdown-items') || o
.dropdown_items
);
482 o
.create_query
= sb
.getAttribute('item-create') || o
.create_query
;
483 o
.create_template
= sb
.getAttribute('item-template') || o
.create_template
;
485 var ul
= sb
.querySelector('ul'),
486 more
= sb
.appendChild(E('span', { class: 'more', tabindex
: -1 }, '···')),
487 open
= sb
.appendChild(E('span', { class: 'open', tabindex
: -1 }, '▾')),
488 canary
= sb
.appendChild(E('div')),
489 create
= sb
.querySelector(this.options
.create_query
),
490 ndisplay
= this.options
.display_items
,
493 if (this.options
.multiple
) {
494 var items
= ul
.querySelectorAll('li');
496 for (var i
= 0; i
< items
.length
; i
++) {
497 this.transformItem(sb
, items
[i
]);
499 if (items
[i
].hasAttribute('selected') && ndisplay
-- > 0)
500 items
[i
].setAttribute('display', n
++);
504 if (this.options
.optional
&& !ul
.querySelector('li[data-value=""]')) {
505 var placeholder
= E('li', { placeholder
: '' },
506 this.options
.select_placeholder
|| this.options
.placeholder
);
509 ? ul
.insertBefore(placeholder
, ul
.firstChild
)
510 : ul
.appendChild(placeholder
);
513 var items
= ul
.querySelectorAll('li'),
514 sel
= sb
.querySelectorAll('[selected]');
516 sel
.forEach(function(s
) {
517 s
.removeAttribute('selected');
520 var s
= sel
[0] || items
[0];
522 s
.setAttribute('selected', '');
523 s
.setAttribute('display', n
++);
529 this.saveValues(sb
, ul
);
531 ul
.setAttribute('tabindex', -1);
532 sb
.setAttribute('tabindex', 0);
535 sb
.setAttribute('more', '')
537 sb
.removeAttribute('more');
539 if (ndisplay
== this.options
.display_items
)
540 sb
.setAttribute('empty', '')
542 sb
.removeAttribute('empty');
544 L
.dom
.content(more
, (ndisplay
== this.options
.display_items
)
545 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
548 sb
.addEventListener('click', this.handleClick
.bind(this));
549 sb
.addEventListener('keydown', this.handleKeydown
.bind(this));
550 sb
.addEventListener('cbi-dropdown-close', this.handleDropdownClose
.bind(this));
551 sb
.addEventListener('cbi-dropdown-select', this.handleDropdownSelect
.bind(this));
553 if ('ontouchstart' in window
) {
554 sb
.addEventListener('touchstart', function(ev
) { ev
.stopPropagation(); });
555 window
.addEventListener('touchstart', this.closeAllDropdowns
);
558 sb
.addEventListener('mouseover', this.handleMouseover
.bind(this));
559 sb
.addEventListener('focus', this.handleFocus
.bind(this));
561 canary
.addEventListener('focus', this.handleCanaryFocus
.bind(this));
563 window
.addEventListener('mouseover', this.setFocus
);
564 window
.addEventListener('click', this.closeAllDropdowns
);
568 create
.addEventListener('keydown', this.handleCreateKeydown
.bind(this));
569 create
.addEventListener('focus', this.handleCreateFocus
.bind(this));
570 create
.addEventListener('blur', this.handleCreateBlur
.bind(this));
572 var li
= findParent(create
, 'li');
574 li
.setAttribute('unselectable', '');
575 li
.addEventListener('click', this.handleCreateClick
.bind(this));
580 this.setUpdateEvents(sb
, 'cbi-dropdown-open', 'cbi-dropdown-close');
581 this.setChangeEvents(sb
, 'cbi-dropdown-change', 'cbi-dropdown-close');
583 L
.dom
.bindClassInstance(sb
, this);
588 openDropdown: function(sb
) {
589 var st
= window
.getComputedStyle(sb
, null),
590 ul
= sb
.querySelector('ul'),
591 li
= ul
.querySelectorAll('li'),
592 fl
= findParent(sb
, '.cbi-value-field'),
593 sel
= ul
.querySelector('[selected]'),
594 rect
= sb
.getBoundingClientRect(),
595 items
= Math
.min(this.options
.dropdown_items
, li
.length
);
597 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
598 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
601 sb
.setAttribute('open', '');
603 var pv
= ul
.cloneNode(true);
604 pv
.classList
.add('preview');
607 fl
.classList
.add('cbi-dropdown-open');
609 if ('ontouchstart' in window
) {
610 var vpWidth
= Math
.max(document
.documentElement
.clientWidth
, window
.innerWidth
|| 0),
611 vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
612 scrollFrom
= window
.pageYOffset
,
613 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
616 ul
.style
.top
= sb
.offsetHeight
+ 'px';
617 ul
.style
.left
= -rect
.left
+ 'px';
618 ul
.style
.right
= (rect
.right
- vpWidth
) + 'px';
619 ul
.style
.maxHeight
= (vpHeight
* 0.5) + 'px';
620 ul
.style
.WebkitOverflowScrolling
= 'touch';
622 var scrollStep = function(timestamp
) {
625 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
628 var duration
= Math
.max(timestamp
- start
, 1);
629 if (duration
< 100) {
630 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
631 window
.requestAnimationFrame(scrollStep
);
634 document
.body
.scrollTop
= scrollTo
;
638 window
.requestAnimationFrame(scrollStep
);
641 ul
.style
.maxHeight
= '1px';
642 ul
.style
.top
= ul
.style
.bottom
= '';
644 window
.requestAnimationFrame(function() {
645 var itemHeight
= li
[Math
.max(0, li
.length
- 2)].getBoundingClientRect().height
,
647 spaceAbove
= rect
.top
,
648 spaceBelow
= window
.innerHeight
- rect
.height
- rect
.top
;
650 for (var i
= 0; i
< (items
== -1 ? li
.length
: items
); i
++)
651 fullHeight
+= li
[i
].getBoundingClientRect().height
;
653 if (fullHeight
<= spaceBelow
) {
654 ul
.style
.top
= rect
.height
+ 'px';
655 ul
.style
.maxHeight
= spaceBelow
+ 'px';
657 else if (fullHeight
<= spaceAbove
) {
658 ul
.style
.bottom
= rect
.height
+ 'px';
659 ul
.style
.maxHeight
= spaceAbove
+ 'px';
661 else if (spaceBelow
>= spaceAbove
) {
662 ul
.style
.top
= rect
.height
+ 'px';
663 ul
.style
.maxHeight
= (spaceBelow
- (spaceBelow
% itemHeight
)) + 'px';
666 ul
.style
.bottom
= rect
.height
+ 'px';
667 ul
.style
.maxHeight
= (spaceAbove
- (spaceAbove
% itemHeight
)) + 'px';
670 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
674 var cboxes
= ul
.querySelectorAll('[selected] input[type="checkbox"]');
675 for (var i
= 0; i
< cboxes
.length
; i
++) {
676 cboxes
[i
].checked
= true;
677 cboxes
[i
].disabled
= (cboxes
.length
== 1 && !this.options
.optional
);
680 ul
.classList
.add('dropdown');
682 sb
.insertBefore(pv
, ul
.nextElementSibling
);
684 li
.forEach(function(l
) {
685 l
.setAttribute('tabindex', 0);
688 sb
.lastElementChild
.setAttribute('tabindex', 0);
690 this.setFocus(sb
, sel
|| li
[0], true);
693 closeDropdown: function(sb
, no_focus
) {
694 if (!sb
.hasAttribute('open'))
697 var pv
= sb
.querySelector('ul.preview'),
698 ul
= sb
.querySelector('ul.dropdown'),
699 li
= ul
.querySelectorAll('li'),
700 fl
= findParent(sb
, '.cbi-value-field');
702 li
.forEach(function(l
) { l
.removeAttribute('tabindex'); });
703 sb
.lastElementChild
.removeAttribute('tabindex');
706 sb
.removeAttribute('open');
707 sb
.style
.width
= sb
.style
.height
= '';
709 ul
.classList
.remove('dropdown');
710 ul
.style
.top
= ul
.style
.bottom
= ul
.style
.maxHeight
= '';
713 fl
.classList
.remove('cbi-dropdown-open');
716 this.setFocus(sb
, sb
);
718 this.saveValues(sb
, ul
);
721 toggleItem: function(sb
, li
, force_state
) {
722 if (li
.hasAttribute('unselectable'))
725 if (this.options
.multiple
) {
726 var cbox
= li
.querySelector('input[type="checkbox"]'),
727 items
= li
.parentNode
.querySelectorAll('li'),
728 label
= sb
.querySelector('ul.preview'),
729 sel
= li
.parentNode
.querySelectorAll('[selected]').length
,
730 more
= sb
.querySelector('.more'),
731 ndisplay
= this.options
.display_items
,
734 if (li
.hasAttribute('selected')) {
735 if (force_state
!== true) {
736 if (sel
> 1 || this.options
.optional
) {
737 li
.removeAttribute('selected');
738 cbox
.checked
= cbox
.disabled
= false;
742 cbox
.disabled
= true;
747 if (force_state
!== false) {
748 li
.setAttribute('selected', '');
750 cbox
.disabled
= false;
755 while (label
&& label
.firstElementChild
)
756 label
.removeChild(label
.firstElementChild
);
758 for (var i
= 0; i
< items
.length
; i
++) {
759 items
[i
].removeAttribute('display');
760 if (items
[i
].hasAttribute('selected')) {
761 if (ndisplay
-- > 0) {
762 items
[i
].setAttribute('display', n
++);
764 label
.appendChild(items
[i
].cloneNode(true));
766 var c
= items
[i
].querySelector('input[type="checkbox"]');
768 c
.disabled
= (sel
== 1 && !this.options
.optional
);
773 sb
.setAttribute('more', '');
775 sb
.removeAttribute('more');
777 if (ndisplay
=== this.options
.display_items
)
778 sb
.setAttribute('empty', '');
780 sb
.removeAttribute('empty');
782 L
.dom
.content(more
, (ndisplay
=== this.options
.display_items
)
783 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
786 var sel
= li
.parentNode
.querySelector('[selected]');
788 sel
.removeAttribute('display');
789 sel
.removeAttribute('selected');
792 li
.setAttribute('display', 0);
793 li
.setAttribute('selected', '');
795 this.closeDropdown(sb
, true);
798 this.saveValues(sb
, li
.parentNode
);
801 transformItem: function(sb
, li
) {
802 var cbox
= E('form', {}, E('input', { type
: 'checkbox', tabindex
: -1, onclick
: 'event.preventDefault()' })),
805 while (li
.firstChild
)
806 label
.appendChild(li
.firstChild
);
808 li
.appendChild(cbox
);
809 li
.appendChild(label
);
812 saveValues: function(sb
, ul
) {
813 var sel
= ul
.querySelectorAll('li[selected]'),
814 div
= sb
.lastElementChild
,
815 name
= this.options
.name
,
819 while (div
.lastElementChild
)
820 div
.removeChild(div
.lastElementChild
);
822 sel
.forEach(function (s
) {
823 if (s
.hasAttribute('placeholder'))
828 value
: s
.hasAttribute('data-value') ? s
.getAttribute('data-value') : s
.innerText
,
832 div
.appendChild(E('input', {
840 strval
+= strval
.length
? ' ' + v
.value
: v
.value
;
848 if (this.options
.multiple
)
849 detail
.values
= values
;
851 detail
.value
= values
.length
? values
[0] : null;
855 sb
.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
861 setValues: function(sb
, values
) {
862 var ul
= sb
.querySelector('ul');
864 if (this.options
.create
) {
865 for (var value
in values
) {
866 this.createItems(sb
, value
);
868 if (!this.options
.multiple
)
873 if (this.options
.multiple
) {
874 var lis
= ul
.querySelectorAll('li[data-value]');
875 for (var i
= 0; i
< lis
.length
; i
++) {
876 var value
= lis
[i
].getAttribute('data-value');
877 if (values
=== null || !(value
in values
))
878 this.toggleItem(sb
, lis
[i
], false);
880 this.toggleItem(sb
, lis
[i
], true);
884 var ph
= ul
.querySelector('li[placeholder]');
886 this.toggleItem(sb
, ph
);
888 var lis
= ul
.querySelectorAll('li[data-value]');
889 for (var i
= 0; i
< lis
.length
; i
++) {
890 var value
= lis
[i
].getAttribute('data-value');
891 if (values
!== null && (value
in values
))
892 this.toggleItem(sb
, lis
[i
]);
897 setFocus: function(sb
, elem
, scroll
) {
898 if (sb
&& sb
.hasAttribute
&& sb
.hasAttribute('locked-in'))
901 if (sb
.target
&& findParent(sb
.target
, 'ul.dropdown'))
904 document
.querySelectorAll('.focus').forEach(function(e
) {
905 if (!matchesElem(e
, 'input')) {
906 e
.classList
.remove('focus');
913 elem
.classList
.add('focus');
916 elem
.parentNode
.scrollTop
= elem
.offsetTop
- elem
.parentNode
.offsetTop
;
920 createChoiceElement: function(sb
, value
, label
) {
921 var tpl
= sb
.querySelector(this.options
.create_template
),
925 markup
= (tpl
.textContent
|| tpl
.innerHTML
|| tpl
.firstChild
.data
).replace(/^<!--|-->$/, '').trim();
927 markup
= '<li data-value="{{value}}"><span data-label-placeholder="true" /></li>';
929 var new_item
= E(markup
.replace(/{{value}}/g, '%h'.format(value
))),
930 placeholder
= new_item
.querySelector('[data-label-placeholder]');
933 var content
= E('span', {}, label
|| this.choices
[value
] || [ value
]);
935 while (content
.firstChild
)
936 placeholder
.parentNode
.insertBefore(content
.firstChild
, placeholder
);
938 placeholder
.parentNode
.removeChild(placeholder
);
941 if (this.options
.multiple
)
942 this.transformItem(sb
, new_item
);
947 createItems: function(sb
, value
) {
949 val
= (value
|| '').trim(),
950 ul
= sb
.querySelector('ul');
952 if (!sbox
.options
.multiple
)
953 val
= val
.length
? [ val
] : [];
955 val
= val
.length
? val
.split(/\s+/) : [];
957 val
.forEach(function(item
) {
960 ul
.childNodes
.forEach(function(li
) {
961 if (li
.getAttribute
&& li
.getAttribute('data-value') === item
)
966 new_item
= sbox
.createChoiceElement(sb
, item
);
968 if (!sbox
.options
.multiple
) {
969 var old
= ul
.querySelector('li[created]');
973 new_item
.setAttribute('created', '');
976 new_item
= ul
.insertBefore(new_item
, ul
.lastElementChild
);
979 sbox
.toggleItem(sb
, new_item
, true);
980 sbox
.setFocus(sb
, new_item
, true);
984 clearChoices: function(reset_value
) {
985 var ul
= this.node
.querySelector('ul'),
986 lis
= ul
? ul
.querySelectorAll('li[data-value]') : [],
987 len
= lis
.length
- (this.options
.create
? 1 : 0),
988 val
= reset_value
? null : this.getValue();
990 for (var i
= 0; i
< len
; i
++) {
991 var lival
= lis
[i
].getAttribute('data-value');
993 (!this.options
.multiple
&& val
!= lival
) ||
994 (this.options
.multiple
&& val
.indexOf(lival
) == -1))
995 ul
.removeChild(lis
[i
]);
999 this.setValues(this.node
, {});
1002 addChoices: function(values
, labels
) {
1004 ul
= sb
.querySelector('ul'),
1005 lis
= ul
? ul
.querySelectorAll('li[data-value]') : [];
1007 if (!Array
.isArray(values
))
1008 values
= L
.toArray(values
);
1010 if (!L
.isObject(labels
))
1013 for (var i
= 0; i
< values
.length
; i
++) {
1016 for (var j
= 0; j
< lis
.length
; j
++) {
1017 if (lis
[j
].getAttribute('data-value') === values
[i
]) {
1027 this.createChoiceElement(sb
, values
[i
], labels
[values
[i
]]),
1028 ul
.lastElementChild
);
1032 closeAllDropdowns: function() {
1033 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1034 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1038 handleClick: function(ev
) {
1039 var sb
= ev
.currentTarget
;
1041 if (!sb
.hasAttribute('open')) {
1042 if (!matchesElem(ev
.target
, 'input'))
1043 this.openDropdown(sb
);
1046 var li
= findParent(ev
.target
, 'li');
1047 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1048 this.toggleItem(sb
, li
);
1049 else if (li
&& li
.parentNode
.classList
.contains('preview'))
1050 this.closeDropdown(sb
);
1051 else if (matchesElem(ev
.target
, 'span.open, span.more'))
1052 this.closeDropdown(sb
);
1055 ev
.preventDefault();
1056 ev
.stopPropagation();
1059 handleKeydown: function(ev
) {
1060 var sb
= ev
.currentTarget
;
1062 if (matchesElem(ev
.target
, 'input'))
1065 if (!sb
.hasAttribute('open')) {
1066 switch (ev
.keyCode
) {
1071 this.openDropdown(sb
);
1072 ev
.preventDefault();
1076 var active
= findParent(document
.activeElement
, 'li');
1078 switch (ev
.keyCode
) {
1080 this.closeDropdown(sb
);
1085 if (!active
.hasAttribute('selected'))
1086 this.toggleItem(sb
, active
);
1087 this.closeDropdown(sb
);
1088 ev
.preventDefault();
1094 this.toggleItem(sb
, active
);
1095 ev
.preventDefault();
1100 if (active
&& active
.previousElementSibling
) {
1101 this.setFocus(sb
, active
.previousElementSibling
);
1102 ev
.preventDefault();
1107 if (active
&& active
.nextElementSibling
) {
1108 this.setFocus(sb
, active
.nextElementSibling
);
1109 ev
.preventDefault();
1116 handleDropdownClose: function(ev
) {
1117 var sb
= ev
.currentTarget
;
1119 this.closeDropdown(sb
, true);
1122 handleDropdownSelect: function(ev
) {
1123 var sb
= ev
.currentTarget
,
1124 li
= findParent(ev
.target
, 'li');
1129 this.toggleItem(sb
, li
);
1130 this.closeDropdown(sb
, true);
1133 handleMouseover: function(ev
) {
1134 var sb
= ev
.currentTarget
;
1136 if (!sb
.hasAttribute('open'))
1139 var li
= findParent(ev
.target
, 'li');
1141 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1142 this.setFocus(sb
, li
);
1145 handleFocus: function(ev
) {
1146 var sb
= ev
.currentTarget
;
1148 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1149 if (s
!== sb
|| sb
.hasAttribute('open'))
1150 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1154 handleCanaryFocus: function(ev
) {
1155 this.closeDropdown(ev
.currentTarget
.parentNode
);
1158 handleCreateKeydown: function(ev
) {
1159 var input
= ev
.currentTarget
,
1160 sb
= findParent(input
, '.cbi-dropdown');
1162 switch (ev
.keyCode
) {
1164 ev
.preventDefault();
1166 if (input
.classList
.contains('cbi-input-invalid'))
1169 this.createItems(sb
, input
.value
);
1176 handleCreateFocus: function(ev
) {
1177 var input
= ev
.currentTarget
,
1178 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1179 sb
= findParent(input
, '.cbi-dropdown');
1182 cbox
.checked
= true;
1184 sb
.setAttribute('locked-in', '');
1187 handleCreateBlur: function(ev
) {
1188 var input
= ev
.currentTarget
,
1189 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1190 sb
= findParent(input
, '.cbi-dropdown');
1193 cbox
.checked
= false;
1195 sb
.removeAttribute('locked-in');
1198 handleCreateClick: function(ev
) {
1199 ev
.currentTarget
.querySelector(this.options
.create_query
).focus();
1202 setValue: function(values
) {
1203 if (this.options
.multiple
) {
1204 if (!Array
.isArray(values
))
1205 values
= (values
!= null && values
!= '') ? [ values
] : [];
1209 for (var i
= 0; i
< values
.length
; i
++)
1210 v
[values
[i
]] = true;
1212 this.setValues(this.node
, v
);
1217 if (values
!= null) {
1218 if (Array
.isArray(values
))
1219 v
[values
[0]] = true;
1224 this.setValues(this.node
, v
);
1228 getValue: function() {
1229 var div
= this.node
.lastElementChild
,
1230 h
= div
.querySelectorAll('input[type="hidden"]'),
1233 for (var i
= 0; i
< h
.length
; i
++)
1236 return this.options
.multiple
? v
: v
[0];
1240 var UICombobox
= UIDropdown
.extend({
1241 __init__: function(value
, choices
, options
) {
1242 this.super('__init__', [ value
, choices
, Object
.assign({
1243 select_placeholder
: _('-- Please choose --'),
1244 custom_placeholder
: _('-- custom --'),
1255 var UIComboButton
= UIDropdown
.extend({
1256 __init__: function(value
, choices
, options
) {
1257 this.super('__init__', [ value
, choices
, Object
.assign({
1266 render: function(/* ... */) {
1267 var node
= UIDropdown
.prototype.render
.apply(this, arguments
),
1268 val
= this.getValue();
1270 if (L
.isObject(this.options
.classes
) && this.options
.classes
.hasOwnProperty(val
))
1271 node
.setAttribute('class', 'cbi-dropdown ' + this.options
.classes
[val
]);
1276 handleClick: function(ev
) {
1277 var sb
= ev
.currentTarget
,
1280 if (sb
.hasAttribute('open') || L
.dom
.matches(t
, '.cbi-dropdown > span.open'))
1281 return UIDropdown
.prototype.handleClick
.apply(this, arguments
);
1283 if (this.options
.click
)
1284 return this.options
.click
.call(sb
, ev
, this.getValue());
1287 toggleItem: function(sb
/*, ... */) {
1288 var rv
= UIDropdown
.prototype.toggleItem
.apply(this, arguments
),
1289 val
= this.getValue();
1291 if (L
.isObject(this.options
.classes
) && this.options
.classes
.hasOwnProperty(val
))
1292 sb
.setAttribute('class', 'cbi-dropdown ' + this.options
.classes
[val
]);
1294 sb
.setAttribute('class', 'cbi-dropdown');
1300 var UIDynamicList
= UIElement
.extend({
1301 __init__: function(values
, choices
, options
) {
1302 if (!Array
.isArray(values
))
1303 values
= (values
!= null && values
!= '') ? [ values
] : [];
1305 if (typeof(choices
) != 'object')
1308 this.values
= values
;
1309 this.choices
= choices
;
1310 this.options
= Object
.assign({}, options
, {
1316 render: function() {
1318 'id': this.options
.id
,
1319 'class': 'cbi-dynlist'
1320 }, E('div', { 'class': 'add-item' }));
1323 var cbox
= new UICombobox(null, this.choices
, this.options
);
1324 dl
.lastElementChild
.appendChild(cbox
.render());
1327 var inputEl
= E('input', {
1328 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1330 'class': 'cbi-input-text',
1331 'placeholder': this.options
.placeholder
1334 dl
.lastElementChild
.appendChild(inputEl
);
1335 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1337 if (this.options
.datatype
|| this.options
.validate
)
1338 L
.ui
.addValidator(inputEl
, this.options
.datatype
|| 'string',
1339 true, this.options
.validate
, 'blur', 'keyup');
1342 for (var i
= 0; i
< this.values
.length
; i
++)
1343 this.addItem(dl
, this.values
[i
],
1344 this.choices
? this.choices
[this.values
[i
]] : null);
1346 return this.bind(dl
);
1349 bind: function(dl
) {
1350 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1351 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1352 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1356 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1357 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1359 L
.dom
.bindClassInstance(dl
, this);
1364 addItem: function(dl
, value
, text
, flash
) {
1366 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1367 E('span', {}, [ text
|| value
]),
1370 'name': this.options
.name
,
1371 'value': value
})]);
1373 dl
.querySelectorAll('.item').forEach(function(item
) {
1377 var hidden
= item
.querySelector('input[type="hidden"]');
1379 if (hidden
&& hidden
.parentNode
!== item
)
1382 if (hidden
&& hidden
.value
=== value
)
1387 var ai
= dl
.querySelector('.add-item');
1388 ai
.parentNode
.insertBefore(new_item
, ai
);
1391 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1402 removeItem: function(dl
, item
) {
1403 var value
= item
.querySelector('input[type="hidden"]').value
;
1404 var sb
= dl
.querySelector('.cbi-dropdown');
1406 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1407 if (li
.getAttribute('data-value') === value
) {
1408 if (li
.hasAttribute('dynlistcustom'))
1409 li
.parentNode
.removeChild(li
);
1411 li
.removeAttribute('unselectable');
1415 item
.parentNode
.removeChild(item
);
1417 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1428 handleClick: function(ev
) {
1429 var dl
= ev
.currentTarget
,
1430 item
= findParent(ev
.target
, '.item');
1433 this.removeItem(dl
, item
);
1435 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1436 var input
= ev
.target
.previousElementSibling
;
1437 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1438 this.addItem(dl
, input
.value
, null, true);
1444 handleDropdownChange: function(ev
) {
1445 var dl
= ev
.currentTarget
,
1446 sbIn
= ev
.detail
.instance
,
1447 sbEl
= ev
.detail
.element
,
1448 sbVal
= ev
.detail
.value
;
1453 sbIn
.setValues(sbEl
, null);
1454 sbVal
.element
.setAttribute('unselectable', '');
1456 if (sbVal
.element
.hasAttribute('created')) {
1457 sbVal
.element
.removeAttribute('created');
1458 sbVal
.element
.setAttribute('dynlistcustom', '');
1461 this.addItem(dl
, sbVal
.value
, sbVal
.text
, true);
1464 handleKeydown: function(ev
) {
1465 var dl
= ev
.currentTarget
,
1466 item
= findParent(ev
.target
, '.item');
1469 switch (ev
.keyCode
) {
1470 case 8: /* backspace */
1471 if (item
.previousElementSibling
)
1472 item
.previousElementSibling
.focus();
1474 this.removeItem(dl
, item
);
1477 case 46: /* delete */
1478 if (item
.nextElementSibling
) {
1479 if (item
.nextElementSibling
.classList
.contains('item'))
1480 item
.nextElementSibling
.focus();
1482 item
.nextElementSibling
.firstElementChild
.focus();
1485 this.removeItem(dl
, item
);
1489 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1490 switch (ev
.keyCode
) {
1491 case 13: /* enter */
1492 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1493 this.addItem(dl
, ev
.target
.value
, null, true);
1494 ev
.target
.value
= '';
1499 ev
.preventDefault();
1505 getValue: function() {
1506 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1507 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1510 for (var i
= 0; i
< items
.length
; i
++)
1511 v
.push(items
[i
].value
);
1513 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1514 input
.classList
.contains('cbi-input-invalid') == false &&
1515 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1516 v
.push(input
.value
);
1521 setValue: function(values
) {
1522 if (!Array
.isArray(values
))
1523 values
= (values
!= null && values
!= '') ? [ values
] : [];
1525 var items
= this.node
.querySelectorAll('.item');
1527 for (var i
= 0; i
< items
.length
; i
++)
1528 if (items
[i
].parentNode
=== this.node
)
1529 this.removeItem(this.node
, items
[i
]);
1531 for (var i
= 0; i
< values
.length
; i
++)
1532 this.addItem(this.node
, values
[i
],
1533 this.choices
? this.choices
[values
[i
]] : null);
1537 var UIHiddenfield
= UIElement
.extend({
1538 __init__: function(value
, options
) {
1540 this.options
= Object
.assign({
1545 render: function() {
1546 var hiddenEl
= E('input', {
1547 'id': this.options
.id
,
1552 return this.bind(hiddenEl
);
1555 bind: function(hiddenEl
) {
1556 this.node
= hiddenEl
;
1558 L
.dom
.bindClassInstance(hiddenEl
, this);
1563 getValue: function() {
1564 return this.node
.value
;
1567 setValue: function(value
) {
1568 this.node
.value
= value
;
1572 var UIFileUpload
= UIElement
.extend({
1573 __init__: function(value
, options
) {
1575 this.options
= Object
.assign({
1577 enable_upload
: true,
1578 enable_remove
: true,
1579 root_directory
: '/etc/luci-uploads'
1583 bind: function(browserEl
) {
1584 this.node
= browserEl
;
1586 this.setUpdateEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1587 this.setChangeEvents(browserEl
, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
1589 L
.dom
.bindClassInstance(browserEl
, this);
1594 render: function() {
1595 return L
.resolveDefault(this.value
!= null ? fs
.stat(this.value
) : null).then(L
.bind(function(stat
) {
1598 if (L
.isObject(stat
) && stat
.type
!= 'directory')
1601 if (this.stat
!= null)
1602 label
= [ this.iconForType(this.stat
.type
), ' %s (%1000mB)'.format(this.truncatePath(this.stat
.path
), this.stat
.size
) ];
1603 else if (this.value
!= null)
1604 label
= [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value
), _('File not accessible')) ];
1606 label
= [ _('Select file…') ];
1608 return this.bind(E('div', { 'id': this.options
.id
}, [
1611 'click': L
.ui
.createHandlerFn(this, 'handleFileBrowser')
1614 'class': 'cbi-filebrowser'
1618 'name': this.options
.name
,
1625 truncatePath: function(path
) {
1626 if (path
.length
> 50)
1627 path
= path
.substring(0, 25) + '…' + path
.substring(path
.length
- 25);
1632 iconForType: function(type
) {
1636 'src': L
.resource('cbi/link.gif'),
1637 'title': _('Symbolic link'),
1643 'src': L
.resource('cbi/folder.gif'),
1644 'title': _('Directory'),
1650 'src': L
.resource('cbi/file.gif'),
1657 canonicalizePath: function(path
) {
1658 return path
.replace(/\/{2,}/, '/')
1659 .replace(/\/\.(\/|$)/g, '/')
1660 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
1661 .replace(/\/$/, '');
1664 splitPath: function(path
) {
1665 var croot
= this.canonicalizePath(this.options
.root_directory
|| '/'),
1666 cpath
= this.canonicalizePath(path
|| '/');
1668 if (cpath
.length
<= croot
.length
)
1671 if (cpath
.charAt(croot
.length
) != '/')
1674 var parts
= cpath
.substring(croot
.length
+ 1).split(/\//);
1676 parts
.unshift(croot
);
1681 handleUpload: function(path
, list
, ev
) {
1682 var form
= ev
.target
.parentNode
,
1683 fileinput
= form
.querySelector('input[type="file"]'),
1684 nameinput
= form
.querySelector('input[type="text"]'),
1685 filename
= (nameinput
.value
!= null ? nameinput
.value
: '').trim();
1687 ev
.preventDefault();
1689 if (filename
== '' || filename
.match(/\//) || fileinput
.files
[0] == null)
1692 var existing
= list
.filter(function(e
) { return e
.name
== filename
})[0];
1694 if (existing
!= null && existing
.type
== 'directory')
1695 return alert(_('A directory with the same name already exists.'));
1696 else if (existing
!= null && !confirm(_('Overwrite existing file "%s" ?').format(filename
)))
1699 var data
= new FormData();
1701 data
.append('sessionid', L
.env
.sessionid
);
1702 data
.append('filename', path
+ '/' + filename
);
1703 data
.append('filedata', fileinput
.files
[0]);
1705 return L
.Request
.post(L
.env
.cgi_base
+ '/cgi-upload', data
, {
1706 progress
: L
.bind(function(btn
, ev
) {
1707 btn
.firstChild
.data
= '%.2f%%'.format((ev
.loaded
/ ev
.total
) * 100);
1709 }).then(L
.bind(function(path
, ev
, res
) {
1710 var reply
= res
.json();
1712 if (L
.isObject(reply
) && reply
.failure
)
1713 alert(_('Upload request failed: %s').format(reply
.message
));
1715 return this.handleSelect(path
, null, ev
);
1716 }, this, path
, ev
));
1719 handleDelete: function(path
, fileStat
, ev
) {
1720 var parent
= path
.replace(/\/[^\/]+$/, '') || '/',
1721 name
= path
.replace(/^.+\//, ''),
1724 ev
.preventDefault();
1726 if (fileStat
.type
== 'directory')
1727 msg
= _('Do you really want to recursively delete the directory "%s" ?').format(name
);
1729 msg
= _('Do you really want to delete "%s" ?').format(name
);
1732 var button
= this.node
.firstElementChild
,
1733 hidden
= this.node
.lastElementChild
;
1735 if (path
== hidden
.value
) {
1736 L
.dom
.content(button
, _('Select file…'));
1740 return fs
.remove(path
).then(L
.bind(function(parent
, ev
) {
1741 return this.handleSelect(parent
, null, ev
);
1742 }, this, parent
, ev
)).catch(function(err
) {
1743 alert(_('Delete request failed: %s').format(err
.message
));
1748 renderUpload: function(path
, list
) {
1749 if (!this.options
.enable_upload
)
1755 'class': 'btn cbi-button-positive',
1756 'click': function(ev
) {
1757 var uploadForm
= ev
.target
.nextElementSibling
,
1758 fileInput
= uploadForm
.querySelector('input[type="file"]');
1760 ev
.target
.style
.display
= 'none';
1761 uploadForm
.style
.display
= '';
1764 }, _('Upload file…')),
1765 E('div', { 'class': 'upload', 'style': 'display:none' }, [
1768 'style': 'display:none',
1769 'change': function(ev
) {
1770 var nameinput
= ev
.target
.parentNode
.querySelector('input[type="text"]'),
1771 uploadbtn
= ev
.target
.parentNode
.querySelector('button.cbi-button-save');
1773 nameinput
.value
= ev
.target
.value
.replace(/^.+[\/\\]/, '');
1774 uploadbtn
.disabled
= false;
1779 'click': function(ev
) {
1780 ev
.preventDefault();
1781 ev
.target
.previousElementSibling
.click();
1783 }, [ _('Browse…') ]),
1784 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
1786 'class': 'btn cbi-button-save',
1787 'click': L
.ui
.createHandlerFn(this, 'handleUpload', path
, list
),
1789 }, [ _('Upload file') ])
1794 renderListing: function(container
, path
, list
) {
1795 var breadcrumb
= E('p'),
1798 list
.sort(function(a
, b
) {
1799 var isDirA
= (a
.type
== 'directory'),
1800 isDirB
= (b
.type
== 'directory');
1802 if (isDirA
!= isDirB
)
1803 return isDirA
< isDirB
;
1805 return a
.name
> b
.name
;
1808 for (var i
= 0; i
< list
.length
; i
++) {
1809 if (!this.options
.show_hidden
&& list
[i
].name
.charAt(0) == '.')
1812 var entrypath
= this.canonicalizePath(path
+ '/' + list
[i
].name
),
1813 selected
= (entrypath
== this.node
.lastElementChild
.value
),
1814 mtime
= new Date(list
[i
].mtime
* 1000);
1816 rows
.appendChild(E('li', [
1817 E('div', { 'class': 'name' }, [
1818 this.iconForType(list
[i
].type
),
1822 'style': selected
? 'font-weight:bold' : null,
1823 'click': L
.ui
.createHandlerFn(this, 'handleSelect',
1824 entrypath
, list
[i
].type
!= 'directory' ? list
[i
] : null)
1825 }, '%h'.format(list
[i
].name
))
1827 E('div', { 'class': 'mtime hide-xs' }, [
1828 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
1829 mtime
.getFullYear(),
1830 mtime
.getMonth() + 1,
1837 selected
? E('button', {
1839 'click': L
.ui
.createHandlerFn(this, 'handleReset')
1840 }, [ _('Deselect') ]) : '',
1841 this.options
.enable_remove
? E('button', {
1842 'class': 'btn cbi-button-negative',
1843 'click': L
.ui
.createHandlerFn(this, 'handleDelete', entrypath
, list
[i
])
1844 }, [ _('Delete') ]) : ''
1849 if (!rows
.firstElementChild
)
1850 rows
.appendChild(E('em', _('No entries in this directory')));
1852 var dirs
= this.splitPath(path
),
1855 for (var i
= 0; i
< dirs
.length
; i
++) {
1856 cur
= cur
? cur
+ '/' + dirs
[i
] : dirs
[i
];
1857 L
.dom
.append(breadcrumb
, [
1861 'click': L
.ui
.createHandlerFn(this, 'handleSelect', cur
|| '/', null)
1862 }, dirs
[i
] != '' ? '%h'.format(dirs
[i
]) : E('em', '(root)')),
1866 L
.dom
.content(container
, [
1869 E('div', { 'class': 'right' }, [
1870 this.renderUpload(path
, list
),
1874 'click': L
.ui
.createHandlerFn(this, 'handleCancel')
1880 handleCancel: function(ev
) {
1881 var button
= this.node
.firstElementChild
,
1882 browser
= button
.nextElementSibling
;
1884 browser
.classList
.remove('open');
1885 button
.style
.display
= '';
1887 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
1890 handleReset: function(ev
) {
1891 var button
= this.node
.firstElementChild
,
1892 hidden
= this.node
.lastElementChild
;
1895 L
.dom
.content(button
, _('Select file…'));
1897 this.handleCancel(ev
);
1900 handleSelect: function(path
, fileStat
, ev
) {
1901 var browser
= L
.dom
.parent(ev
.target
, '.cbi-filebrowser'),
1902 ul
= browser
.querySelector('ul');
1904 if (fileStat
== null) {
1905 L
.dom
.content(ul
, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
1906 L
.resolveDefault(fs
.list(path
), []).then(L
.bind(this.renderListing
, this, browser
, path
));
1909 var button
= this.node
.firstElementChild
,
1910 hidden
= this.node
.lastElementChild
;
1912 path
= this.canonicalizePath(path
);
1914 L
.dom
.content(button
, [
1915 this.iconForType(fileStat
.type
),
1916 ' %s (%1000mB)'.format(this.truncatePath(path
), fileStat
.size
)
1919 browser
.classList
.remove('open');
1920 button
.style
.display
= '';
1921 hidden
.value
= path
;
1923 this.stat
= Object
.assign({ path
: path
}, fileStat
);
1924 this.node
.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail
: this.stat
}));
1928 handleFileBrowser: function(ev
) {
1929 var button
= ev
.target
,
1930 browser
= button
.nextElementSibling
,
1931 path
= this.stat
? this.stat
.path
.replace(/\/[^\/]+$/, '') : this.options
.root_directory
;
1933 if (this.options
.root_directory
.indexOf(path
) != 0)
1934 path
= this.options
.root_directory
;
1936 ev
.preventDefault();
1938 return L
.resolveDefault(fs
.list(path
), []).then(L
.bind(function(button
, browser
, path
, list
) {
1939 document
.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl
) {
1940 L
.dom
.findClassInstance(browserEl
).handleCancel(ev
);
1943 button
.style
.display
= 'none';
1944 browser
.classList
.add('open');
1946 return this.renderListing(browser
, path
, list
);
1947 }, this, button
, browser
, path
));
1950 getValue: function() {
1951 return this.node
.lastElementChild
.value
;
1954 setValue: function(value
) {
1955 this.node
.lastElementChild
.value
= value
;
1960 return L
.Class
.extend({
1961 __init__: function() {
1962 modalDiv
= document
.body
.appendChild(
1963 L
.dom
.create('div', { id
: 'modal_overlay' },
1964 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1966 tooltipDiv
= document
.body
.appendChild(
1967 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1969 /* setup old aliases */
1970 L
.showModal
= this.showModal
;
1971 L
.hideModal
= this.hideModal
;
1972 L
.showTooltip
= this.showTooltip
;
1973 L
.hideTooltip
= this.hideTooltip
;
1974 L
.itemlist
= this.itemlist
;
1976 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1977 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1978 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1979 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1981 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1982 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1983 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1987 showModal: function(title
, children
/* , ... */) {
1988 var dlg
= modalDiv
.firstElementChild
;
1990 dlg
.setAttribute('class', 'modal');
1992 for (var i
= 2; i
< arguments
.length
; i
++)
1993 dlg
.classList
.add(arguments
[i
]);
1995 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1996 L
.dom
.append(dlg
, children
);
1998 document
.body
.classList
.add('modal-overlay-active');
2003 hideModal: function() {
2004 document
.body
.classList
.remove('modal-overlay-active');
2008 showTooltip: function(ev
) {
2009 var target
= findParent(ev
.target
, '[data-tooltip]');
2014 if (tooltipTimeout
!== null) {
2015 window
.clearTimeout(tooltipTimeout
);
2016 tooltipTimeout
= null;
2019 var rect
= target
.getBoundingClientRect(),
2020 x
= rect
.left
+ window
.pageXOffset
,
2021 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
2023 tooltipDiv
.className
= 'cbi-tooltip';
2024 tooltipDiv
.innerHTML
= '▲ ';
2025 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
2027 if (target
.hasAttribute('data-tooltip-style'))
2028 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
2030 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
2031 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
2032 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
2035 tooltipDiv
.style
.top
= y
+ 'px';
2036 tooltipDiv
.style
.left
= x
+ 'px';
2037 tooltipDiv
.style
.opacity
= 1;
2039 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
2041 detail
: { target
: target
}
2045 hideTooltip: function(ev
) {
2046 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
2047 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
2050 if (tooltipTimeout
!== null) {
2051 window
.clearTimeout(tooltipTimeout
);
2052 tooltipTimeout
= null;
2055 tooltipDiv
.style
.opacity
= 0;
2056 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
2058 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
2061 addNotification: function(title
, children
/*, ... */) {
2062 var mc
= document
.querySelector('#maincontent') || document
.body
;
2063 var msg
= E('div', {
2064 'class': 'alert-message fade-in',
2065 'style': 'display:flex',
2066 'transitionend': function(ev
) {
2067 var node
= ev
.currentTarget
;
2068 if (node
.parentNode
&& node
.classList
.contains('fade-out'))
2069 node
.parentNode
.removeChild(node
);
2072 E('div', { 'style': 'flex:10' }),
2073 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
2076 'style': 'margin-left:auto; margin-top:auto',
2077 'click': function(ev
) {
2078 L
.dom
.parent(ev
.target
, '.alert-message').classList
.add('fade-out');
2081 }, [ _('Dismiss') ])
2086 L
.dom
.append(msg
.firstElementChild
, E('h4', {}, title
));
2088 L
.dom
.append(msg
.firstElementChild
, children
);
2090 for (var i
= 2; i
< arguments
.length
; i
++)
2091 msg
.classList
.add(arguments
[i
]);
2093 mc
.insertBefore(msg
, mc
.firstElementChild
);
2099 itemlist: function(node
, items
, separators
) {
2102 if (!Array
.isArray(separators
))
2103 separators
= [ separators
|| E('br') ];
2105 for (var i
= 0; i
< items
.length
; i
+= 2) {
2106 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
2107 var sep
= separators
[(i
/2) % separators
.length
],
2110 children
.push(E('span', { class: 'nowrap' }, [
2111 items
[i
] ? E('strong', items
[i
] + ': ') : '',
2115 if ((i
+2) < items
.length
)
2116 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
2120 L
.dom
.content(node
, children
);
2126 tabs
: L
.Class
.singleton({
2128 var groups
= [], prevGroup
= null, currGroup
= null;
2130 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2131 var parent
= tab
.parentNode
;
2133 if (L
.dom
.matches(tab
, 'li') && L
.dom
.matches(parent
, 'ul.cbi-tabmenu'))
2136 if (!parent
.hasAttribute('data-tab-group'))
2137 parent
.setAttribute('data-tab-group', groups
.length
);
2139 currGroup
= +parent
.getAttribute('data-tab-group');
2141 if (currGroup
!== prevGroup
) {
2142 prevGroup
= currGroup
;
2144 if (!groups
[currGroup
])
2145 groups
[currGroup
] = [];
2148 groups
[currGroup
].push(tab
);
2151 for (var i
= 0; i
< groups
.length
; i
++)
2152 this.initTabGroup(groups
[i
]);
2154 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
2159 initTabGroup: function(panes
) {
2160 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
2163 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
2164 group
= panes
[0].parentNode
,
2165 groupId
= +group
.getAttribute('data-tab-group'),
2168 if (group
.getAttribute('data-initialized') === 'true')
2171 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
2172 var name
= pane
.getAttribute('data-tab'),
2173 title
= pane
.getAttribute('data-tab-title'),
2174 active
= pane
.getAttribute('data-tab-active') === 'true';
2176 menu
.appendChild(E('li', {
2177 'style': this.isEmptyPane(pane
) ? 'display:none' : null,
2178 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
2182 'click': this.switchTab
.bind(this)
2189 group
.parentNode
.insertBefore(menu
, group
);
2190 group
.setAttribute('data-initialized', true);
2192 if (selected
=== null) {
2193 selected
= this.getActiveTabId(panes
[0]);
2195 if (selected
< 0 || selected
>= panes
.length
|| this.isEmptyPane(panes
[selected
])) {
2196 for (var i
= 0; i
< panes
.length
; i
++) {
2197 if (!this.isEmptyPane(panes
[i
])) {
2204 menu
.childNodes
[selected
].classList
.add('cbi-tab');
2205 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
2206 panes
[selected
].setAttribute('data-tab-active', 'true');
2208 this.setActiveTabId(panes
[selected
], selected
);
2211 this.updateTabs(group
);
2214 isEmptyPane: function(pane
) {
2215 return L
.dom
.isEmpty(pane
, function(n
) { return n
.classList
.contains('cbi-tab-descr') });
2218 getPathForPane: function(pane
) {
2219 var path
= [], node
= null;
2221 for (node
= pane
? pane
.parentNode
: null;
2222 node
!= null && node
.hasAttribute
!= null;
2223 node
= node
.parentNode
)
2225 if (node
.hasAttribute('data-tab'))
2226 path
.unshift(node
.getAttribute('data-tab'));
2227 else if (node
.hasAttribute('data-section-id'))
2228 path
.unshift(node
.getAttribute('data-section-id'));
2231 return path
.join('/');
2234 getActiveTabState: function() {
2235 var page
= document
.body
.getAttribute('data-page');
2238 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
2239 if (val
.page
=== page
&& L
.isObject(val
.paths
))
2244 window
.sessionStorage
.removeItem('tab');
2245 return { page
: page
, paths
: {} };
2248 getActiveTabId: function(pane
) {
2249 var path
= this.getPathForPane(pane
);
2250 return +this.getActiveTabState().paths
[path
] || 0;
2253 setActiveTabId: function(pane
, tabIndex
) {
2254 var path
= this.getPathForPane(pane
);
2257 var state
= this.getActiveTabState();
2258 state
.paths
[path
] = tabIndex
;
2260 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
2262 catch (e
) { return false; }
2267 updateTabs: function(ev
, root
) {
2268 (root
|| document
).querySelectorAll('[data-tab-title]').forEach(L
.bind(function(pane
) {
2269 var menu
= pane
.parentNode
.previousElementSibling
,
2270 tab
= menu
? menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))) : null,
2271 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
2276 if (this.isEmptyPane(pane
)) {
2277 tab
.style
.display
= 'none';
2278 tab
.classList
.remove('flash');
2280 else if (tab
.style
.display
=== 'none') {
2281 tab
.style
.display
= '';
2282 requestAnimationFrame(function() { tab
.classList
.add('flash') });
2286 tab
.setAttribute('data-errors', n_errors
);
2287 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
2288 tab
.setAttribute('data-tooltip-style', 'error');
2291 tab
.removeAttribute('data-errors');
2292 tab
.removeAttribute('data-tooltip');
2297 switchTab: function(ev
) {
2298 var tab
= ev
.target
.parentNode
,
2299 name
= tab
.getAttribute('data-tab'),
2300 menu
= tab
.parentNode
,
2301 group
= menu
.nextElementSibling
,
2302 groupId
= +group
.getAttribute('data-tab-group'),
2305 ev
.preventDefault();
2307 if (!tab
.classList
.contains('cbi-tab-disabled'))
2310 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
2311 tab
.classList
.remove('cbi-tab');
2312 tab
.classList
.remove('cbi-tab-disabled');
2314 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
2317 group
.childNodes
.forEach(function(pane
) {
2318 if (L
.dom
.matches(pane
, '[data-tab]')) {
2319 if (pane
.getAttribute('data-tab') === name
) {
2320 pane
.setAttribute('data-tab-active', 'true');
2321 L
.ui
.tabs
.setActiveTabId(pane
, index
);
2324 pane
.setAttribute('data-tab-active', 'false');
2333 /* File uploading */
2334 uploadFile: function(path
, progressStatusNode
) {
2335 return new Promise(function(resolveFn
, rejectFn
) {
2336 L
.ui
.showModal(_('Uploading file…'), [
2337 E('p', _('Please select the file to upload.')),
2338 E('div', { 'style': 'display:flex' }, [
2339 E('div', { 'class': 'left', 'style': 'flex:1' }, [
2342 style
: 'display:none',
2343 change: function(ev
) {
2344 var modal
= L
.dom
.parent(ev
.target
, '.modal'),
2345 body
= modal
.querySelector('p'),
2346 upload
= modal
.querySelector('.cbi-button-action.important'),
2347 file
= ev
.currentTarget
.files
[0];
2352 L
.dom
.content(body
, [
2354 E('li', {}, [ '%s: %s'.format(_('Name'), file
.name
.replace(/^.*[\\\/]/, '')) ]),
2355 E('li', {}, [ '%s: %1024mB'.format(_('Size'), file
.size
) ])
2359 upload
.disabled
= false;
2365 'click': function(ev
) {
2366 ev
.target
.previousElementSibling
.click();
2368 }, [ _('Browse…') ])
2370 E('div', { 'class': 'right', 'style': 'flex:1' }, [
2373 'click': function() {
2375 rejectFn(new Error('Upload has been cancelled'));
2377 }, [ _('Cancel') ]),
2380 'class': 'btn cbi-button-action important',
2382 'click': function(ev
) {
2383 var input
= L
.dom
.parent(ev
.target
, '.modal').querySelector('input[type="file"]');
2385 if (!input
.files
[0])
2388 var progress
= E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
2390 L
.ui
.showModal(_('Uploading file…'), [ progress
]);
2392 var data
= new FormData();
2394 data
.append('sessionid', rpc
.getSessionID());
2395 data
.append('filename', path
);
2396 data
.append('filedata', input
.files
[0]);
2398 var filename
= input
.files
[0].name
;
2400 L
.Request
.post(L
.env
.cgi_base
+ '/cgi-upload', data
, {
2402 progress: function(pev
) {
2403 var percent
= (pev
.loaded
/ pev
.total
) * 100;
2405 if (progressStatusNode
)
2406 progressStatusNode
.data
= '%.2f%%'.format(percent
);
2408 progress
.setAttribute('title', '%.2f%%'.format(percent
));
2409 progress
.firstElementChild
.style
.width
= '%.2f%%'.format(percent
);
2411 }).then(function(res
) {
2412 var reply
= res
.json();
2416 if (L
.isObject(reply
) && reply
.failure
) {
2417 L
.ui
.addNotification(null, E('p', _('Upload request failed: %s').format(reply
.message
)));
2418 rejectFn(new Error(reply
.failure
));
2421 reply
.name
= filename
;
2436 /* Reconnect handling */
2437 pingDevice: function(proto
, ipaddr
) {
2438 var target
= '%s://%s%s?%s'.format(proto
|| 'http', ipaddr
|| window
.location
.host
, L
.resource('icons/loading.gif'), Math
.random());
2440 return new Promise(function(resolveFn
, rejectFn
) {
2441 var img
= new Image();
2443 img
.onload
= resolveFn
;
2444 img
.onerror
= rejectFn
;
2446 window
.setTimeout(rejectFn
, 1000);
2452 awaitReconnect: function(/* ... */) {
2453 var ipaddrs
= arguments
.length
? arguments
: [ window
.location
.host
];
2455 window
.setTimeout(L
.bind(function() {
2456 L
.Poll
.add(L
.bind(function() {
2457 var tasks
= [], reachable
= false;
2459 for (var i
= 0; i
< 2; i
++)
2460 for (var j
= 0; j
< ipaddrs
.length
; j
++)
2461 tasks
.push(this.pingDevice(i
? 'https' : 'http', ipaddrs
[j
])
2462 .then(function(ev
) { reachable
= ev
.target
.src
.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
2464 return Promise
.all(tasks
).then(function() {
2467 window
.location
= reachable
;
2475 changes
: L
.Class
.singleton({
2477 if (!L
.env
.sessionid
)
2480 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
2483 setIndicator: function(n
) {
2484 var i
= document
.querySelector('.uci_change_indicator');
2486 var poll
= document
.getElementById('xhr_poll_status');
2487 i
= poll
.parentNode
.insertBefore(E('a', {
2489 'class': 'uci_change_indicator label notice',
2490 'click': L
.bind(this.displayChanges
, this)
2495 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
2496 i
.classList
.add('flash');
2497 i
.style
.display
= '';
2500 i
.classList
.remove('flash');
2501 i
.style
.display
= 'none';
2505 renderChangeIndicator: function(changes
) {
2508 for (var config
in changes
)
2509 if (changes
.hasOwnProperty(config
))
2510 n_changes
+= changes
[config
].length
;
2512 this.changes
= changes
;
2513 this.setIndicator(n_changes
);
2517 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
2518 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
2519 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
2520 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
2521 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
2522 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
2523 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
2524 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
2525 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
2526 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
2529 displayChanges: function() {
2530 var list
= E('div', { 'class': 'uci-change-list' }),
2531 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
2532 E('div', { 'class': 'cbi-section' }, [
2533 E('strong', _('Legend:')),
2534 E('div', { 'class': 'uci-change-legend' }, [
2535 E('div', { 'class': 'uci-change-legend-label' }, [
2536 E('ins', ' '), ' ', _('Section added') ]),
2537 E('div', { 'class': 'uci-change-legend-label' }, [
2538 E('del', ' '), ' ', _('Section removed') ]),
2539 E('div', { 'class': 'uci-change-legend-label' }, [
2540 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
2541 E('div', { 'class': 'uci-change-legend-label' }, [
2542 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
2544 E('div', { 'class': 'right' }, [
2547 'click': L
.ui
.hideModal
2548 }, [ _('Dismiss') ]), ' ',
2550 'class': 'cbi-button cbi-button-positive important',
2551 'click': L
.bind(this.apply
, this, true)
2552 }, [ _('Save & Apply') ]), ' ',
2554 'class': 'cbi-button cbi-button-reset',
2555 'click': L
.bind(this.revert
, this)
2556 }, [ _('Revert') ])])])
2559 for (var config
in this.changes
) {
2560 if (!this.changes
.hasOwnProperty(config
))
2563 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
2565 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
2566 var chg
= this.changes
[config
][i
],
2567 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
2569 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
2575 if (added
!= null && chg
[1] == added
[0])
2576 return '@' + added
[1] + '[-1]';
2581 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
2588 if (chg[0] == 'add')
2589 added = [ chg[1], chg[2] ];
2593 list.appendChild(E('br'));
2594 dlg.classList.add('uci-dialog');
2597 displayStatus: function(type, content) {
2599 var message = L.ui.showModal('', '');
2601 message.classList.add('alert-message');
2602 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
2605 L.dom.content(message, content);
2607 if (!this.was_polling) {
2608 this.was_polling = L.Request.poll.active();
2609 L.Request.poll.stop();
2615 if (this.was_polling)
2616 L.Request.poll.start();
2620 rollback: function(checked) {
2622 this.displayStatus('warning spinning',
2623 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
2624 .format(L.env.apply_rollback)));
2626 var call = function(r, data, duration) {
2627 if (r.status === 204) {
2628 L.ui.changes.displayStatus('warning', [
2629 E('h4', _('Configuration changes have been rolled back!')),
2630 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)),
2631 E('div', { 'class': 'right' }, [
2634 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
2635 }, [ _('Dismiss') ]), ' ',
2637 'class': 'btn cbi-button-action important',
2638 'click': L.bind(L.ui.changes.revert, L.ui.changes)
2639 }, [ _('Revert changes') ]), ' ',
2641 'class': 'btn cbi-button-negative important',
2642 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
2643 }, [ _('Apply unchecked') ])
2650 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2651 window.setTimeout(function() {
2652 L.Request.request(L.url('admin/uci/confirm'), {
2654 timeout: L.env.apply_timeout * 1000,
2655 query: { sid: L.env.sessionid, token: L.env.token }
2660 call({ status: 0 });
2663 this.displayStatus('warning', [
2664 E('h4', _('Device unreachable!')),
2665 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.'))
2670 confirm: function(checked, deadline, override_token) {
2672 var ts = Date.now();
2674 this.displayStatus('notice');
2677 this.confirm_auth = { token: override_token };
2679 var call = function(r, data, duration) {
2680 if (Date.now() >= deadline) {
2681 window.clearTimeout(tt);
2682 L.ui.changes.rollback(checked);
2685 else if (r && (r.status === 200 || r.status === 204)) {
2686 document.dispatchEvent(new CustomEvent('uci-applied'));
2688 L.ui.changes.setIndicator(0);
2689 L.ui.changes.displayStatus('notice',
2690 E('p', _('Configuration changes applied.')));
2692 window.clearTimeout(tt);
2693 window.setTimeout(function() {
2694 //L.ui.changes.displayStatus(false);
2695 window.location = window.location.href.split('#')[0];
2696 }, L.env.apply_display * 1000);
2701 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
2702 window.setTimeout(function() {
2703 L.Request.request(L.url('admin/uci/confirm'), {
2705 timeout: L.env.apply_timeout * 1000,
2706 query: L.ui.changes.confirm_auth
2707 }).then(call, call);
2711 var tick = function() {
2712 var now = Date.now();
2714 L.ui.changes.displayStatus('notice spinning',
2715 E('p', _('Applying configuration changes… %ds')
2716 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
2718 if (now >= deadline)
2721 tt = window.setTimeout(tick, 1000 - (now - ts));
2727 /* wait a few seconds for the settings to become effective */
2728 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
2731 apply: function(checked) {
2732 this.displayStatus('notice spinning',
2733 E('p', _('Starting configuration apply…')));
2735 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
2737 query: { sid: L.env.sessionid, token: L.env.token }
2738 }).then(function(r) {
2739 if (r.status === (checked ? 200 : 204)) {
2740 var tok = null; try { tok = r.json(); } catch(e) {}
2741 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
2742 L.ui.changes.confirm_auth = tok;
2744 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
2746 else if (checked && r.status === 204) {
2747 L.ui.changes.displayStatus('notice',
2748 E('p', _('There are no changes to apply')));
2750 window.setTimeout(function() {
2751 L.ui.changes.displayStatus(false);
2752 }, L.env.apply_display * 1000);
2755 L.ui.changes.displayStatus('warning',
2756 E('p', _('Apply request failed with status <code>%h</code>')
2757 .format(r.responseText || r.statusText || r.status)));
2759 window.setTimeout(function() {
2760 L.ui.changes.displayStatus(false);
2761 }, L.env.apply_display * 1000);
2766 revert: function() {
2767 this.displayStatus('notice spinning',
2768 E('p', _('Reverting configuration…')));
2770 L.Request.request(L.url('admin/uci/revert'), {
2772 query: { sid: L.env.sessionid, token: L.env.token }
2773 }).then(function(r) {
2774 if (r.status === 200) {
2775 document.dispatchEvent(new CustomEvent('uci-reverted'));
2777 L.ui.changes.setIndicator(0);
2778 L.ui.changes.displayStatus('notice',
2779 E('p', _('Changes have been reverted.')));
2781 window.setTimeout(function() {
2782 //L.ui.changes.displayStatus(false);
2783 window.location = window.location.href.split('#')[0];
2784 }, L.env.apply_display * 1000);
2787 L.ui.changes.displayStatus('warning',
2788 E('p', _('Revert request failed with status <code>%h</code>')
2789 .format(r.statusText || r.status)));
2791 window.setTimeout(function() {
2792 L.ui.changes.displayStatus(false);
2793 }, L.env.apply_display * 1000);
2799 addValidator: function(field, type, optional, vfunc /*, ... */) {
2803 var events = this.varargs(arguments, 3);
2804 if (events.length == 0)
2805 events.push('blur', 'keyup');
2808 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2809 validatorFn = cbiValidator.validate.bind(cbiValidator);
2811 for (var i = 0; i < events.length; i++)
2812 field.addEventListener(events[i], validatorFn);
2821 createHandlerFn: function(ctx, fn /*, ... */) {
2822 if (typeof(fn) == 'string')
2825 if (typeof(fn) != 'function')
2828 var arg_offset = arguments.length - 2;
2830 return Function.prototype.bind.apply(function() {
2831 var t = arguments[arg_offset].target;
2833 t.classList.add('spinning');
2839 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
2840 t.classList.remove('spinning');
2843 }, this.varargs(arguments, 2, ctx));
2846 AbstractElement: UIElement,
2849 Textfield: UITextfield,
2850 Textarea: UITextarea,
2851 Checkbox: UICheckbox,
2853 Dropdown: UIDropdown,
2854 DynamicList: UIDynamicList,
2855 Combobox: UICombobox,
2856 ComboButton: UIComboButton,
2857 Hiddenfield: UIHiddenfield,
2858 FileUpload: UIFileUpload