93e947d01d4feaf5109026cb07ec47e2e3cc44bc
9 var UIElement
= L
.Class
.extend({
10 getValue: function() {
11 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
12 return this.node
.value
;
17 setValue: function(value
) {
18 if (L
.dom
.matches(this.node
, 'select') || L
.dom
.matches(this.node
, 'input'))
19 this.node
.value
= value
;
23 return (this.validState
!== false);
26 triggerValidation: function() {
27 if (typeof(this.vfunc
) != 'function')
30 var wasValid
= this.isValid();
34 return (wasValid
!= this.isValid());
37 registerEvents: function(targetNode
, synevent
, events
) {
38 var dispatchFn
= L
.bind(function(ev
) {
39 this.node
.dispatchEvent(new CustomEvent(synevent
, { bubbles
: true }));
42 for (var i
= 0; i
< events
.length
; i
++)
43 targetNode
.addEventListener(events
[i
], dispatchFn
);
46 setUpdateEvents: function(targetNode
/*, ... */) {
47 var datatype
= this.options
.datatype
,
48 optional
= this.options
.hasOwnProperty('optional') ? this.options
.optional
: true,
49 validate
= this.options
.validate
,
50 events
= this.varargs(arguments
, 1);
52 this.registerEvents(targetNode
, 'widget-update', events
);
54 if (!datatype
&& !validate
)
57 this.vfunc
= L
.ui
.addValidator
.apply(L
.ui
, [
58 targetNode
, datatype
|| 'string',
62 this.node
.addEventListener('validation-success', L
.bind(function(ev
) {
63 this.validState
= true;
66 this.node
.addEventListener('validation-failure', L
.bind(function(ev
) {
67 this.validState
= false;
71 setChangeEvents: function(targetNode
/*, ... */) {
72 this.registerEvents(targetNode
, 'widget-change', this.varargs(arguments
, 1));
76 var UITextfield
= UIElement
.extend({
77 __init__: function(value
, options
) {
79 this.options
= Object
.assign({
86 var frameEl
= E('div', { 'id': this.options
.id
});
88 if (this.options
.password
) {
89 frameEl
.classList
.add('nowrap');
90 frameEl
.appendChild(E('input', {
92 'style': 'position:absolute; left:-100000px',
95 'name': this.options
.name
? 'password.%s'.format(this.options
.name
) : null
99 frameEl
.appendChild(E('input', {
100 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
101 'name': this.options
.name
,
102 'type': this.options
.password
? 'password' : 'text',
103 'class': this.options
.password
? 'cbi-input-password' : 'cbi-input-text',
104 'readonly': this.options
.readonly
? '' : null,
105 'maxlength': this.options
.maxlength
,
106 'placeholder': this.options
.placeholder
,
110 if (this.options
.password
)
111 frameEl
.appendChild(E('button', {
112 'class': 'cbi-button cbi-button-neutral',
113 'title': _('Reveal/hide password'),
114 'aria-label': _('Reveal/hide password'),
115 'click': function(ev
) {
116 var e
= this.previousElementSibling
;
117 e
.type
= (e
.type
=== 'password') ? 'text' : 'password';
122 return this.bind(frameEl
);
125 bind: function(frameEl
) {
126 var inputEl
= frameEl
.childNodes
[+!!this.options
.password
];
130 this.setUpdateEvents(inputEl
, 'keyup', 'blur');
131 this.setChangeEvents(inputEl
, 'change');
133 L
.dom
.bindClassInstance(frameEl
, this);
138 getValue: function() {
139 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
140 return inputEl
.value
;
143 setValue: function(value
) {
144 var inputEl
= this.node
.childNodes
[+!!this.options
.password
];
145 inputEl
.value
= value
;
149 var UICheckbox
= UIElement
.extend({
150 __init__: function(value
, options
) {
152 this.options
= Object
.assign({
159 var frameEl
= E('div', {
160 'id': this.options
.id
,
161 'class': 'cbi-checkbox'
164 if (this.options
.hiddenname
)
165 frameEl
.appendChild(E('input', {
167 'name': this.options
.hiddenname
,
171 frameEl
.appendChild(E('input', {
172 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
173 'name': this.options
.name
,
175 'value': this.options
.value_enabled
,
176 'checked': (this.value
== this.options
.value_enabled
) ? '' : null
179 return this.bind(frameEl
);
182 bind: function(frameEl
) {
185 this.setUpdateEvents(frameEl
.lastElementChild
, 'click', 'blur');
186 this.setChangeEvents(frameEl
.lastElementChild
, 'change');
188 L
.dom
.bindClassInstance(frameEl
, this);
193 isChecked: function() {
194 return this.node
.lastElementChild
.checked
;
197 getValue: function() {
198 return this.isChecked()
199 ? this.options
.value_enabled
200 : this.options
.value_disabled
;
203 setValue: function(value
) {
204 this.node
.lastElementChild
.checked
= (value
== this.options
.value_enabled
);
208 var UISelect
= UIElement
.extend({
209 __init__: function(value
, choices
, options
) {
210 if (typeof(choices
) != 'object')
213 if (!Array
.isArray(value
))
214 value
= (value
!= null && value
!= '') ? [ value
] : [];
216 if (!options
.multiple
&& value
.length
> 1)
220 this.choices
= choices
;
221 this.options
= Object
.assign({
224 orientation
: 'horizontal'
229 var frameEl
= E('div', { 'id': this.options
.id
}),
230 keys
= Object
.keys(this.choices
);
232 if (this.options
.sort
=== true)
234 else if (Array
.isArray(this.options
.sort
))
235 keys
= this.options
.sort
;
237 if (this.options
.widget
== 'select') {
238 frameEl
.appendChild(E('select', {
239 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
240 'name': this.options
.name
,
241 'size': this.options
.size
,
242 'class': 'cbi-input-select',
243 'multiple': this.options
.multiple
? '' : null
246 if (this.options
.optional
|| this.choices
.hasOwnProperty(''))
247 frameEl
.lastChild
.appendChild(E('option', {
249 'selected': (this.values
.length
== 0 || this.values
[0] == '') ? '' : null
250 }, this.choices
[''] || this.options
.placeholder
|| _('-- Please choose --')));
252 for (var i
= 0; i
< keys
.length
; i
++) {
253 if (keys
[i
] == null || keys
[i
] == '')
256 frameEl
.lastChild
.appendChild(E('option', {
258 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
259 }, this.choices
[keys
[i
]] || keys
[i
]));
263 var brEl
= (this.options
.orientation
=== 'horizontal') ? document
.createTextNode(' ') : E('br');
265 for (var i
= 0; i
< keys
.length
; i
++) {
266 frameEl
.appendChild(E('label', {}, [
268 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
269 'name': this.options
.id
|| this.options
.name
,
270 'type': this.options
.multiple
? 'checkbox' : 'radio',
271 'class': this.options
.multiple
? 'cbi-input-checkbox' : 'cbi-input-radio',
273 'checked': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
275 this.choices
[keys
[i
]] || keys
[i
]
278 if (i
+ 1 == this.options
.size
)
279 frameEl
.appendChild(brEl
);
283 return this.bind(frameEl
);
286 bind: function(frameEl
) {
289 if (this.options
.widget
== 'select') {
290 this.setUpdateEvents(frameEl
.firstChild
, 'change', 'click', 'blur');
291 this.setChangeEvents(frameEl
.firstChild
, 'change');
294 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
295 for (var i
= 0; i
< radioEls
.length
; i
++) {
296 this.setUpdateEvents(radioEls
[i
], 'change', 'click', 'blur');
297 this.setChangeEvents(radioEls
[i
], 'change', 'click', 'blur');
301 L
.dom
.bindClassInstance(frameEl
, this);
306 getValue: function() {
307 if (this.options
.widget
== 'select')
308 return this.node
.firstChild
.value
;
310 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
311 for (var i
= 0; i
< radioEls
.length
; i
++)
312 if (radioEls
[i
].checked
)
313 return radioEls
[i
].value
;
318 setValue: function(value
) {
319 if (this.options
.widget
== 'select') {
323 for (var i
= 0; i
< this.node
.firstChild
.options
.length
; i
++)
324 this.node
.firstChild
.options
[i
].selected
= (this.node
.firstChild
.options
[i
].value
== value
);
329 var radioEls
= frameEl
.querySelectorAll('input[type="radio"]');
330 for (var i
= 0; i
< radioEls
.length
; i
++)
331 radioEls
[i
].checked
= (radioEls
[i
].value
== value
);
335 var UIDropdown
= UIElement
.extend({
336 __init__: function(value
, choices
, options
) {
337 if (typeof(choices
) != 'object')
340 if (!Array
.isArray(value
))
341 this.values
= (value
!= null && value
!= '') ? [ value
] : [];
345 this.choices
= choices
;
346 this.options
= Object
.assign({
348 multiple
: Array
.isArray(value
),
350 select_placeholder
: _('-- Please choose --'),
351 custom_placeholder
: _('-- custom --'),
355 create_query
: '.create-item-input',
356 create_template
: 'script[type="item-template"]'
362 'id': this.options
.id
,
363 'class': 'cbi-dropdown',
364 'multiple': this.options
.multiple
? '' : null,
365 'optional': this.options
.optional
? '' : null,
368 var keys
= Object
.keys(this.choices
);
370 if (this.options
.sort
=== true)
372 else if (Array
.isArray(this.options
.sort
))
373 keys
= this.options
.sort
;
375 if (this.options
.create
)
376 for (var i
= 0; i
< this.values
.length
; i
++)
377 if (!this.choices
.hasOwnProperty(this.values
[i
]))
378 keys
.push(this.values
[i
]);
380 for (var i
= 0; i
< keys
.length
; i
++)
381 sb
.lastElementChild
.appendChild(E('li', {
382 'data-value': keys
[i
],
383 'selected': (this.values
.indexOf(keys
[i
]) > -1) ? '' : null
384 }, this.choices
[keys
[i
]] || keys
[i
]));
386 if (this.options
.create
) {
387 var createEl
= E('input', {
389 'class': 'create-item-input',
390 'readonly': this.options
.readonly
? '' : null,
391 'maxlength': this.options
.maxlength
,
392 'placeholder': this.options
.custom_placeholder
|| this.options
.placeholder
395 if (this.options
.datatype
)
396 L
.ui
.addValidator(createEl
, this.options
.datatype
,
397 true, null, 'blur', 'keyup');
399 sb
.lastElementChild
.appendChild(E('li', { 'data-value': '-' }, createEl
));
402 if (this.options
.create_markup
)
403 sb
.appendChild(E('script', { type
: 'item-template' },
404 this.options
.create_markup
));
406 return this.bind(sb
);
410 var o
= this.options
;
412 o
.multiple
= sb
.hasAttribute('multiple');
413 o
.optional
= sb
.hasAttribute('optional');
414 o
.placeholder
= sb
.getAttribute('placeholder') || o
.placeholder
;
415 o
.display_items
= parseInt(sb
.getAttribute('display-items') || o
.display_items
);
416 o
.dropdown_items
= parseInt(sb
.getAttribute('dropdown-items') || o
.dropdown_items
);
417 o
.create_query
= sb
.getAttribute('item-create') || o
.create_query
;
418 o
.create_template
= sb
.getAttribute('item-template') || o
.create_template
;
420 var ul
= sb
.querySelector('ul'),
421 more
= sb
.appendChild(E('span', { class: 'more', tabindex
: -1 }, '···')),
422 open
= sb
.appendChild(E('span', { class: 'open', tabindex
: -1 }, '▾')),
423 canary
= sb
.appendChild(E('div')),
424 create
= sb
.querySelector(this.options
.create_query
),
425 ndisplay
= this.options
.display_items
,
428 if (this.options
.multiple
) {
429 var items
= ul
.querySelectorAll('li');
431 for (var i
= 0; i
< items
.length
; i
++) {
432 this.transformItem(sb
, items
[i
]);
434 if (items
[i
].hasAttribute('selected') && ndisplay
-- > 0)
435 items
[i
].setAttribute('display', n
++);
439 if (this.options
.optional
&& !ul
.querySelector('li[data-value=""]')) {
440 var placeholder
= E('li', { placeholder
: '' },
441 this.options
.select_placeholder
|| this.options
.placeholder
);
444 ? ul
.insertBefore(placeholder
, ul
.firstChild
)
445 : ul
.appendChild(placeholder
);
448 var items
= ul
.querySelectorAll('li'),
449 sel
= sb
.querySelectorAll('[selected]');
451 sel
.forEach(function(s
) {
452 s
.removeAttribute('selected');
455 var s
= sel
[0] || items
[0];
457 s
.setAttribute('selected', '');
458 s
.setAttribute('display', n
++);
464 this.saveValues(sb
, ul
);
466 ul
.setAttribute('tabindex', -1);
467 sb
.setAttribute('tabindex', 0);
470 sb
.setAttribute('more', '')
472 sb
.removeAttribute('more');
474 if (ndisplay
== this.options
.display_items
)
475 sb
.setAttribute('empty', '')
477 sb
.removeAttribute('empty');
479 L
.dom
.content(more
, (ndisplay
== this.options
.display_items
)
480 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
483 sb
.addEventListener('click', this.handleClick
.bind(this));
484 sb
.addEventListener('keydown', this.handleKeydown
.bind(this));
485 sb
.addEventListener('cbi-dropdown-close', this.handleDropdownClose
.bind(this));
486 sb
.addEventListener('cbi-dropdown-select', this.handleDropdownSelect
.bind(this));
488 if ('ontouchstart' in window
) {
489 sb
.addEventListener('touchstart', function(ev
) { ev
.stopPropagation(); });
490 window
.addEventListener('touchstart', this.closeAllDropdowns
);
493 sb
.addEventListener('mouseover', this.handleMouseover
.bind(this));
494 sb
.addEventListener('focus', this.handleFocus
.bind(this));
496 canary
.addEventListener('focus', this.handleCanaryFocus
.bind(this));
498 window
.addEventListener('mouseover', this.setFocus
);
499 window
.addEventListener('click', this.closeAllDropdowns
);
503 create
.addEventListener('keydown', this.handleCreateKeydown
.bind(this));
504 create
.addEventListener('focus', this.handleCreateFocus
.bind(this));
505 create
.addEventListener('blur', this.handleCreateBlur
.bind(this));
507 var li
= findParent(create
, 'li');
509 li
.setAttribute('unselectable', '');
510 li
.addEventListener('click', this.handleCreateClick
.bind(this));
515 this.setUpdateEvents(sb
, 'cbi-dropdown-open', 'cbi-dropdown-close');
516 this.setChangeEvents(sb
, 'cbi-dropdown-change', 'cbi-dropdown-close');
518 L
.dom
.bindClassInstance(sb
, this);
523 openDropdown: function(sb
) {
524 var st
= window
.getComputedStyle(sb
, null),
525 ul
= sb
.querySelector('ul'),
526 li
= ul
.querySelectorAll('li'),
527 fl
= findParent(sb
, '.cbi-value-field'),
528 sel
= ul
.querySelector('[selected]'),
529 rect
= sb
.getBoundingClientRect(),
530 items
= Math
.min(this.options
.dropdown_items
, li
.length
);
532 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
533 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
536 sb
.setAttribute('open', '');
538 var pv
= ul
.cloneNode(true);
539 pv
.classList
.add('preview');
542 fl
.classList
.add('cbi-dropdown-open');
544 if ('ontouchstart' in window
) {
545 var vpWidth
= Math
.max(document
.documentElement
.clientWidth
, window
.innerWidth
|| 0),
546 vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
547 scrollFrom
= window
.pageYOffset
,
548 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
551 ul
.style
.top
= sb
.offsetHeight
+ 'px';
552 ul
.style
.left
= -rect
.left
+ 'px';
553 ul
.style
.right
= (rect
.right
- vpWidth
) + 'px';
554 ul
.style
.maxHeight
= (vpHeight
* 0.5) + 'px';
555 ul
.style
.WebkitOverflowScrolling
= 'touch';
557 var scrollStep = function(timestamp
) {
560 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
563 var duration
= Math
.max(timestamp
- start
, 1);
564 if (duration
< 100) {
565 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
566 window
.requestAnimationFrame(scrollStep
);
569 document
.body
.scrollTop
= scrollTo
;
573 window
.requestAnimationFrame(scrollStep
);
576 ul
.style
.maxHeight
= '1px';
577 ul
.style
.top
= ul
.style
.bottom
= '';
579 window
.requestAnimationFrame(function() {
580 var itemHeight
= li
[Math
.max(0, li
.length
- 2)].getBoundingClientRect().height
,
582 spaceAbove
= rect
.top
,
583 spaceBelow
= window
.innerHeight
- rect
.height
- rect
.top
;
585 for (var i
= 0; i
< (items
== -1 ? li
.length
: items
); i
++)
586 fullHeight
+= li
[i
].getBoundingClientRect().height
;
588 if (fullHeight
<= spaceBelow
) {
589 ul
.style
.top
= rect
.height
+ 'px';
590 ul
.style
.maxHeight
= spaceBelow
+ 'px';
592 else if (fullHeight
<= spaceAbove
) {
593 ul
.style
.bottom
= rect
.height
+ 'px';
594 ul
.style
.maxHeight
= spaceAbove
+ 'px';
596 else if (spaceBelow
>= spaceAbove
) {
597 ul
.style
.top
= rect
.height
+ 'px';
598 ul
.style
.maxHeight
= (spaceBelow
- (spaceBelow
% itemHeight
)) + 'px';
601 ul
.style
.bottom
= rect
.height
+ 'px';
602 ul
.style
.maxHeight
= (spaceAbove
- (spaceAbove
% itemHeight
)) + 'px';
605 ul
.scrollTop
= sel
? Math
.max(sel
.offsetTop
- sel
.offsetHeight
, 0) : 0;
609 var cboxes
= ul
.querySelectorAll('[selected] input[type="checkbox"]');
610 for (var i
= 0; i
< cboxes
.length
; i
++) {
611 cboxes
[i
].checked
= true;
612 cboxes
[i
].disabled
= (cboxes
.length
== 1 && !this.options
.optional
);
615 ul
.classList
.add('dropdown');
617 sb
.insertBefore(pv
, ul
.nextElementSibling
);
619 li
.forEach(function(l
) {
620 l
.setAttribute('tabindex', 0);
623 sb
.lastElementChild
.setAttribute('tabindex', 0);
625 this.setFocus(sb
, sel
|| li
[0], true);
628 closeDropdown: function(sb
, no_focus
) {
629 if (!sb
.hasAttribute('open'))
632 var pv
= sb
.querySelector('ul.preview'),
633 ul
= sb
.querySelector('ul.dropdown'),
634 li
= ul
.querySelectorAll('li'),
635 fl
= findParent(sb
, '.cbi-value-field');
637 li
.forEach(function(l
) { l
.removeAttribute('tabindex'); });
638 sb
.lastElementChild
.removeAttribute('tabindex');
641 sb
.removeAttribute('open');
642 sb
.style
.width
= sb
.style
.height
= '';
644 ul
.classList
.remove('dropdown');
645 ul
.style
.top
= ul
.style
.bottom
= ul
.style
.maxHeight
= '';
648 fl
.classList
.remove('cbi-dropdown-open');
651 this.setFocus(sb
, sb
);
653 this.saveValues(sb
, ul
);
656 toggleItem: function(sb
, li
, force_state
) {
657 if (li
.hasAttribute('unselectable'))
660 if (this.options
.multiple
) {
661 var cbox
= li
.querySelector('input[type="checkbox"]'),
662 items
= li
.parentNode
.querySelectorAll('li'),
663 label
= sb
.querySelector('ul.preview'),
664 sel
= li
.parentNode
.querySelectorAll('[selected]').length
,
665 more
= sb
.querySelector('.more'),
666 ndisplay
= this.options
.display_items
,
669 if (li
.hasAttribute('selected')) {
670 if (force_state
!== true) {
671 if (sel
> 1 || this.options
.optional
) {
672 li
.removeAttribute('selected');
673 cbox
.checked
= cbox
.disabled
= false;
677 cbox
.disabled
= true;
682 if (force_state
!== false) {
683 li
.setAttribute('selected', '');
685 cbox
.disabled
= false;
690 while (label
&& label
.firstElementChild
)
691 label
.removeChild(label
.firstElementChild
);
693 for (var i
= 0; i
< items
.length
; i
++) {
694 items
[i
].removeAttribute('display');
695 if (items
[i
].hasAttribute('selected')) {
696 if (ndisplay
-- > 0) {
697 items
[i
].setAttribute('display', n
++);
699 label
.appendChild(items
[i
].cloneNode(true));
701 var c
= items
[i
].querySelector('input[type="checkbox"]');
703 c
.disabled
= (sel
== 1 && !this.options
.optional
);
708 sb
.setAttribute('more', '');
710 sb
.removeAttribute('more');
712 if (ndisplay
=== this.options
.display_items
)
713 sb
.setAttribute('empty', '');
715 sb
.removeAttribute('empty');
717 L
.dom
.content(more
, (ndisplay
=== this.options
.display_items
)
718 ? (this.options
.select_placeholder
|| this.options
.placeholder
) : '···');
721 var sel
= li
.parentNode
.querySelector('[selected]');
723 sel
.removeAttribute('display');
724 sel
.removeAttribute('selected');
727 li
.setAttribute('display', 0);
728 li
.setAttribute('selected', '');
730 this.closeDropdown(sb
, true);
733 this.saveValues(sb
, li
.parentNode
);
736 transformItem: function(sb
, li
) {
737 var cbox
= E('form', {}, E('input', { type
: 'checkbox', tabindex
: -1, onclick
: 'event.preventDefault()' })),
740 while (li
.firstChild
)
741 label
.appendChild(li
.firstChild
);
743 li
.appendChild(cbox
);
744 li
.appendChild(label
);
747 saveValues: function(sb
, ul
) {
748 var sel
= ul
.querySelectorAll('li[selected]'),
749 div
= sb
.lastElementChild
,
750 name
= this.options
.name
,
754 while (div
.lastElementChild
)
755 div
.removeChild(div
.lastElementChild
);
757 sel
.forEach(function (s
) {
758 if (s
.hasAttribute('placeholder'))
763 value
: s
.hasAttribute('data-value') ? s
.getAttribute('data-value') : s
.innerText
,
767 div
.appendChild(E('input', {
775 strval
+= strval
.length
? ' ' + v
.value
: v
.value
;
783 if (this.options
.multiple
)
784 detail
.values
= values
;
786 detail
.value
= values
.length
? values
[0] : null;
790 sb
.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
796 setValues: function(sb
, values
) {
797 var ul
= sb
.querySelector('ul');
799 if (this.options
.create
) {
800 for (var value
in values
) {
801 this.createItems(sb
, value
);
803 if (!this.options
.multiple
)
808 if (this.options
.multiple
) {
809 var lis
= ul
.querySelectorAll('li[data-value]');
810 for (var i
= 0; i
< lis
.length
; i
++) {
811 var value
= lis
[i
].getAttribute('data-value');
812 if (values
=== null || !(value
in values
))
813 this.toggleItem(sb
, lis
[i
], false);
815 this.toggleItem(sb
, lis
[i
], true);
819 var ph
= ul
.querySelector('li[placeholder]');
821 this.toggleItem(sb
, ph
);
823 var lis
= ul
.querySelectorAll('li[data-value]');
824 for (var i
= 0; i
< lis
.length
; i
++) {
825 var value
= lis
[i
].getAttribute('data-value');
826 if (values
!== null && (value
in values
))
827 this.toggleItem(sb
, lis
[i
]);
832 setFocus: function(sb
, elem
, scroll
) {
833 if (sb
&& sb
.hasAttribute
&& sb
.hasAttribute('locked-in'))
836 if (sb
.target
&& findParent(sb
.target
, 'ul.dropdown'))
839 document
.querySelectorAll('.focus').forEach(function(e
) {
840 if (!matchesElem(e
, 'input')) {
841 e
.classList
.remove('focus');
848 elem
.classList
.add('focus');
851 elem
.parentNode
.scrollTop
= elem
.offsetTop
- elem
.parentNode
.offsetTop
;
855 createItems: function(sb
, value
) {
857 val
= (value
|| '').trim(),
858 ul
= sb
.querySelector('ul');
860 if (!sbox
.options
.multiple
)
861 val
= val
.length
? [ val
] : [];
863 val
= val
.length
? val
.split(/\s+/) : [];
865 val
.forEach(function(item
) {
868 ul
.childNodes
.forEach(function(li
) {
869 if (li
.getAttribute
&& li
.getAttribute('data-value') === item
)
875 tpl
= sb
.querySelector(sbox
.options
.create_template
);
878 markup
= (tpl
.textContent
|| tpl
.innerHTML
|| tpl
.firstChild
.data
).replace(/^<!--|-->$/, '').trim();
880 markup
= '<li data-value="{{value}}">{{value}}</li>';
882 new_item
= E(markup
.replace(/{{value}}/g, '%h'.format(item
)));
884 if (sbox
.options
.multiple
) {
885 sbox
.transformItem(sb
, new_item
);
888 var old
= ul
.querySelector('li[created]');
892 new_item
.setAttribute('created', '');
895 new_item
= ul
.insertBefore(new_item
, ul
.lastElementChild
);
898 sbox
.toggleItem(sb
, new_item
, true);
899 sbox
.setFocus(sb
, new_item
, true);
903 closeAllDropdowns: function() {
904 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
905 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
909 handleClick: function(ev
) {
910 var sb
= ev
.currentTarget
;
912 if (!sb
.hasAttribute('open')) {
913 if (!matchesElem(ev
.target
, 'input'))
914 this.openDropdown(sb
);
917 var li
= findParent(ev
.target
, 'li');
918 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
919 this.toggleItem(sb
, li
);
920 else if (li
&& li
.parentNode
.classList
.contains('preview'))
921 this.closeDropdown(sb
);
922 else if (matchesElem(ev
.target
, 'span.open, span.more'))
923 this.closeDropdown(sb
);
927 ev
.stopPropagation();
930 handleKeydown: function(ev
) {
931 var sb
= ev
.currentTarget
;
933 if (matchesElem(ev
.target
, 'input'))
936 if (!sb
.hasAttribute('open')) {
937 switch (ev
.keyCode
) {
942 this.openDropdown(sb
);
947 var active
= findParent(document
.activeElement
, 'li');
949 switch (ev
.keyCode
) {
951 this.closeDropdown(sb
);
956 if (!active
.hasAttribute('selected'))
957 this.toggleItem(sb
, active
);
958 this.closeDropdown(sb
);
965 this.toggleItem(sb
, active
);
971 if (active
&& active
.previousElementSibling
) {
972 this.setFocus(sb
, active
.previousElementSibling
);
978 if (active
&& active
.nextElementSibling
) {
979 this.setFocus(sb
, active
.nextElementSibling
);
987 handleDropdownClose: function(ev
) {
988 var sb
= ev
.currentTarget
;
990 this.closeDropdown(sb
, true);
993 handleDropdownSelect: function(ev
) {
994 var sb
= ev
.currentTarget
,
995 li
= findParent(ev
.target
, 'li');
1000 this.toggleItem(sb
, li
);
1001 this.closeDropdown(sb
, true);
1004 handleMouseover: function(ev
) {
1005 var sb
= ev
.currentTarget
;
1007 if (!sb
.hasAttribute('open'))
1010 var li
= findParent(ev
.target
, 'li');
1012 if (li
&& li
.parentNode
.classList
.contains('dropdown'))
1013 this.setFocus(sb
, li
);
1016 handleFocus: function(ev
) {
1017 var sb
= ev
.currentTarget
;
1019 document
.querySelectorAll('.cbi-dropdown[open]').forEach(function(s
) {
1020 if (s
!== sb
|| sb
.hasAttribute('open'))
1021 s
.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1025 handleCanaryFocus: function(ev
) {
1026 this.closeDropdown(ev
.currentTarget
.parentNode
);
1029 handleCreateKeydown: function(ev
) {
1030 var input
= ev
.currentTarget
,
1031 sb
= findParent(input
, '.cbi-dropdown');
1033 switch (ev
.keyCode
) {
1035 ev
.preventDefault();
1037 if (input
.classList
.contains('cbi-input-invalid'))
1040 this.createItems(sb
, input
.value
);
1047 handleCreateFocus: function(ev
) {
1048 var input
= ev
.currentTarget
,
1049 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1050 sb
= findParent(input
, '.cbi-dropdown');
1053 cbox
.checked
= true;
1055 sb
.setAttribute('locked-in', '');
1058 handleCreateBlur: function(ev
) {
1059 var input
= ev
.currentTarget
,
1060 cbox
= findParent(input
, 'li').querySelector('input[type="checkbox"]'),
1061 sb
= findParent(input
, '.cbi-dropdown');
1064 cbox
.checked
= false;
1066 sb
.removeAttribute('locked-in');
1069 handleCreateClick: function(ev
) {
1070 ev
.currentTarget
.querySelector(this.options
.create_query
).focus();
1073 setValue: function(values
) {
1074 if (this.options
.multiple
) {
1075 if (!Array
.isArray(values
))
1076 values
= (values
!= null && values
!= '') ? [ values
] : [];
1080 for (var i
= 0; i
< values
.length
; i
++)
1081 v
[values
[i
]] = true;
1083 this.setValues(this.node
, v
);
1088 if (values
!= null) {
1089 if (Array
.isArray(values
))
1090 v
[values
[0]] = true;
1095 this.setValues(this.node
, v
);
1099 getValue: function() {
1100 var div
= this.node
.lastElementChild
,
1101 h
= div
.querySelectorAll('input[type="hidden"]'),
1104 for (var i
= 0; i
< h
.length
; i
++)
1107 return this.options
.multiple
? v
: v
[0];
1111 var UICombobox
= UIDropdown
.extend({
1112 __init__: function(value
, choices
, options
) {
1113 this.super('__init__', [ value
, choices
, Object
.assign({
1114 select_placeholder
: _('-- Please choose --'),
1115 custom_placeholder
: _('-- custom --'),
1126 var UIDynamicList
= UIElement
.extend({
1127 __init__: function(values
, choices
, options
) {
1128 if (!Array
.isArray(values
))
1129 values
= (values
!= null && values
!= '') ? [ values
] : [];
1131 if (typeof(choices
) != 'object')
1134 this.values
= values
;
1135 this.choices
= choices
;
1136 this.options
= Object
.assign({}, options
, {
1142 render: function() {
1144 'id': this.options
.id
,
1145 'class': 'cbi-dynlist'
1146 }, E('div', { 'class': 'add-item' }));
1149 var cbox
= new UICombobox(null, this.choices
, this.options
);
1150 dl
.lastElementChild
.appendChild(cbox
.render());
1153 var inputEl
= E('input', {
1154 'id': this.options
.id
? 'widget.' + this.options
.id
: null,
1156 'class': 'cbi-input-text',
1157 'placeholder': this.options
.placeholder
1160 dl
.lastElementChild
.appendChild(inputEl
);
1161 dl
.lastElementChild
.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
1163 if (this.options
.datatype
)
1164 L
.ui
.addValidator(inputEl
, this.options
.datatype
,
1165 true, null, 'blur', 'keyup');
1168 for (var i
= 0; i
< this.values
.length
; i
++)
1169 this.addItem(dl
, this.values
[i
],
1170 this.choices
? this.choices
[this.values
[i
]] : null);
1172 return this.bind(dl
);
1175 bind: function(dl
) {
1176 dl
.addEventListener('click', L
.bind(this.handleClick
, this));
1177 dl
.addEventListener('keydown', L
.bind(this.handleKeydown
, this));
1178 dl
.addEventListener('cbi-dropdown-change', L
.bind(this.handleDropdownChange
, this));
1182 this.setUpdateEvents(dl
, 'cbi-dynlist-change');
1183 this.setChangeEvents(dl
, 'cbi-dynlist-change');
1185 L
.dom
.bindClassInstance(dl
, this);
1190 addItem: function(dl
, value
, text
, flash
) {
1192 new_item
= E('div', { 'class': flash
? 'item flash' : 'item', 'tabindex': 0 }, [
1193 E('span', {}, text
|| value
),
1196 'name': this.options
.name
,
1197 'value': value
})]);
1199 dl
.querySelectorAll('.item, .add-item').forEach(function(item
) {
1203 var hidden
= item
.querySelector('input[type="hidden"]');
1205 if (hidden
&& hidden
.parentNode
!== item
)
1208 if (hidden
&& hidden
.value
=== value
)
1210 else if (!hidden
|| hidden
.value
>= value
)
1211 exists
= !!item
.parentNode
.insertBefore(new_item
, item
);
1214 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1225 removeItem: function(dl
, item
) {
1226 var value
= item
.querySelector('input[type="hidden"]').value
;
1227 var sb
= dl
.querySelector('.cbi-dropdown');
1229 sb
.querySelectorAll('ul > li').forEach(function(li
) {
1230 if (li
.getAttribute('data-value') === value
) {
1231 if (li
.hasAttribute('dynlistcustom'))
1232 li
.parentNode
.removeChild(li
);
1234 li
.removeAttribute('unselectable');
1238 item
.parentNode
.removeChild(item
);
1240 dl
.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
1251 handleClick: function(ev
) {
1252 var dl
= ev
.currentTarget
,
1253 item
= findParent(ev
.target
, '.item');
1256 this.removeItem(dl
, item
);
1258 else if (matchesElem(ev
.target
, '.cbi-button-add')) {
1259 var input
= ev
.target
.previousElementSibling
;
1260 if (input
.value
.length
&& !input
.classList
.contains('cbi-input-invalid')) {
1261 this.addItem(dl
, input
.value
, null, true);
1267 handleDropdownChange: function(ev
) {
1268 var dl
= ev
.currentTarget
,
1269 sbIn
= ev
.detail
.instance
,
1270 sbEl
= ev
.detail
.element
,
1271 sbVal
= ev
.detail
.value
;
1276 sbIn
.setValues(sbEl
, null);
1277 sbVal
.element
.setAttribute('unselectable', '');
1279 if (sbVal
.element
.hasAttribute('created')) {
1280 sbVal
.element
.removeAttribute('created');
1281 sbVal
.element
.setAttribute('dynlistcustom', '');
1284 this.addItem(dl
, sbVal
.value
, sbVal
.text
, true);
1287 handleKeydown: function(ev
) {
1288 var dl
= ev
.currentTarget
,
1289 item
= findParent(ev
.target
, '.item');
1292 switch (ev
.keyCode
) {
1293 case 8: /* backspace */
1294 if (item
.previousElementSibling
)
1295 item
.previousElementSibling
.focus();
1297 this.removeItem(dl
, item
);
1300 case 46: /* delete */
1301 if (item
.nextElementSibling
) {
1302 if (item
.nextElementSibling
.classList
.contains('item'))
1303 item
.nextElementSibling
.focus();
1305 item
.nextElementSibling
.firstElementChild
.focus();
1308 this.removeItem(dl
, item
);
1312 else if (matchesElem(ev
.target
, '.cbi-input-text')) {
1313 switch (ev
.keyCode
) {
1314 case 13: /* enter */
1315 if (ev
.target
.value
.length
&& !ev
.target
.classList
.contains('cbi-input-invalid')) {
1316 this.addItem(dl
, ev
.target
.value
, null, true);
1317 ev
.target
.value
= '';
1322 ev
.preventDefault();
1328 getValue: function() {
1329 var items
= this.node
.querySelectorAll('.item > input[type="hidden"]'),
1330 input
= this.node
.querySelector('.add-item > input[type="text"]'),
1333 for (var i
= 0; i
< items
.length
; i
++)
1334 v
.push(items
[i
].value
);
1336 if (input
&& input
.value
!= null && input
.value
.match(/\S/) &&
1337 v
.filter(function(s
) { return s
== input
.value
}).length
== 0)
1338 v
.push(input
.value
);
1343 setValue: function(values
) {
1344 if (!Array
.isArray(values
))
1345 values
= (values
!= null && values
!= '') ? [ values
] : [];
1347 var items
= this.node
.querySelectorAll('.item');
1349 for (var i
= 0; i
< items
.length
; i
++)
1350 if (items
[i
].parentNode
=== this.node
)
1351 this.removeItem(this.node
, items
[i
]);
1353 for (var i
= 0; i
< values
.length
; i
++)
1354 this.addItem(this.node
, values
[i
],
1355 this.choices
? this.choices
[values
[i
]] : null);
1359 var UIHiddenfield
= UIElement
.extend({
1360 __init__: function(value
, options
) {
1362 this.options
= Object
.assign({
1367 render: function() {
1368 var hiddenEl
= E('input', {
1369 'id': this.options
.id
,
1374 return this.bind(hiddenEl
);
1377 bind: function(hiddenEl
) {
1378 this.node
= hiddenEl
;
1380 L
.dom
.bindClassInstance(hiddenEl
, this);
1385 getValue: function() {
1386 return this.node
.value
;
1389 setValue: function(value
) {
1390 this.node
.value
= value
;
1395 return L
.Class
.extend({
1396 __init__: function() {
1397 modalDiv
= document
.body
.appendChild(
1398 L
.dom
.create('div', { id
: 'modal_overlay' },
1399 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1401 tooltipDiv
= document
.body
.appendChild(
1402 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1404 /* setup old aliases */
1405 L
.showModal
= this.showModal
;
1406 L
.hideModal
= this.hideModal
;
1407 L
.showTooltip
= this.showTooltip
;
1408 L
.hideTooltip
= this.hideTooltip
;
1409 L
.itemlist
= this.itemlist
;
1411 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1412 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1413 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1414 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1416 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1417 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1418 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1422 showModal: function(title
, children
/* , ... */) {
1423 var dlg
= modalDiv
.firstElementChild
;
1425 dlg
.setAttribute('class', 'modal');
1427 for (var i
= 2; i
< arguments
.length
; i
++)
1428 dlg
.classList
.add(arguments
[i
]);
1430 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1431 L
.dom
.append(dlg
, children
);
1433 document
.body
.classList
.add('modal-overlay-active');
1438 hideModal: function() {
1439 document
.body
.classList
.remove('modal-overlay-active');
1443 showTooltip: function(ev
) {
1444 var target
= findParent(ev
.target
, '[data-tooltip]');
1449 if (tooltipTimeout
!== null) {
1450 window
.clearTimeout(tooltipTimeout
);
1451 tooltipTimeout
= null;
1454 var rect
= target
.getBoundingClientRect(),
1455 x
= rect
.left
+ window
.pageXOffset
,
1456 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
1458 tooltipDiv
.className
= 'cbi-tooltip';
1459 tooltipDiv
.innerHTML
= '▲ ';
1460 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
1462 if (target
.hasAttribute('data-tooltip-style'))
1463 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
1465 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
1466 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
1467 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
1470 tooltipDiv
.style
.top
= y
+ 'px';
1471 tooltipDiv
.style
.left
= x
+ 'px';
1472 tooltipDiv
.style
.opacity
= 1;
1474 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
1476 detail
: { target
: target
}
1480 hideTooltip: function(ev
) {
1481 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
1482 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
1485 if (tooltipTimeout
!== null) {
1486 window
.clearTimeout(tooltipTimeout
);
1487 tooltipTimeout
= null;
1490 tooltipDiv
.style
.opacity
= 0;
1491 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
1493 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
1497 itemlist: function(node
, items
, separators
) {
1500 if (!Array
.isArray(separators
))
1501 separators
= [ separators
|| E('br') ];
1503 for (var i
= 0; i
< items
.length
; i
+= 2) {
1504 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
1505 var sep
= separators
[(i
/2) % separators
.length
],
1508 children
.push(E('span', { class: 'nowrap' }, [
1509 items
[i
] ? E('strong', items
[i
] + ': ') : '',
1513 if ((i
+2) < items
.length
)
1514 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
1518 L
.dom
.content(node
, children
);
1524 tabs
: L
.Class
.singleton({
1526 var groups
= [], prevGroup
= null, currGroup
= null;
1528 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
1529 var parent
= tab
.parentNode
;
1531 if (!parent
.hasAttribute('data-tab-group'))
1532 parent
.setAttribute('data-tab-group', groups
.length
);
1534 currGroup
= +parent
.getAttribute('data-tab-group');
1536 if (currGroup
!== prevGroup
) {
1537 prevGroup
= currGroup
;
1539 if (!groups
[currGroup
])
1540 groups
[currGroup
] = [];
1543 groups
[currGroup
].push(tab
);
1546 for (var i
= 0; i
< groups
.length
; i
++)
1547 this.initTabGroup(groups
[i
]);
1549 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
1554 this.setActiveTabId(-1, -1);
1557 initTabGroup: function(panes
) {
1558 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
1561 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
1562 group
= panes
[0].parentNode
,
1563 groupId
= +group
.getAttribute('data-tab-group'),
1566 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
1567 var name
= pane
.getAttribute('data-tab'),
1568 title
= pane
.getAttribute('data-tab-title'),
1569 active
= pane
.getAttribute('data-tab-active') === 'true';
1571 menu
.appendChild(E('li', {
1572 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
1576 'click': this.switchTab
.bind(this)
1583 group
.parentNode
.insertBefore(menu
, group
);
1585 if (selected
=== null) {
1586 selected
= this.getActiveTabId(groupId
);
1588 if (selected
< 0 || selected
>= panes
.length
)
1591 menu
.childNodes
[selected
].classList
.add('cbi-tab');
1592 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
1593 panes
[selected
].setAttribute('data-tab-active', 'true');
1595 this.setActiveTabId(groupId
, selected
);
1599 getActiveTabState: function() {
1600 var page
= document
.body
.getAttribute('data-page');
1603 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
1604 if (val
.page
=== page
&& Array
.isArray(val
.groups
))
1609 window
.sessionStorage
.removeItem('tab');
1610 return { page
: page
, groups
: [] };
1613 getActiveTabId: function(groupId
) {
1614 return +this.getActiveTabState().groups
[groupId
] || 0;
1617 setActiveTabId: function(groupId
, tabIndex
) {
1619 var state
= this.getActiveTabState();
1620 state
.groups
[groupId
] = tabIndex
;
1622 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
1624 catch (e
) { return false; }
1629 updateTabs: function(ev
) {
1630 document
.querySelectorAll('[data-tab-title]').forEach(function(pane
) {
1631 var menu
= pane
.parentNode
.previousElementSibling
,
1632 tab
= menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))),
1633 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
1635 if (!pane
.firstElementChild
) {
1636 tab
.style
.display
= 'none';
1637 tab
.classList
.remove('flash');
1639 else if (tab
.style
.display
=== 'none') {
1640 tab
.style
.display
= '';
1641 requestAnimationFrame(function() { tab
.classList
.add('flash') });
1645 tab
.setAttribute('data-errors', n_errors
);
1646 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
1647 tab
.setAttribute('data-tooltip-style', 'error');
1650 tab
.removeAttribute('data-errors');
1651 tab
.removeAttribute('data-tooltip');
1656 switchTab: function(ev
) {
1657 var tab
= ev
.target
.parentNode
,
1658 name
= tab
.getAttribute('data-tab'),
1659 menu
= tab
.parentNode
,
1660 group
= menu
.nextElementSibling
,
1661 groupId
= +group
.getAttribute('data-tab-group'),
1664 ev
.preventDefault();
1666 if (!tab
.classList
.contains('cbi-tab-disabled'))
1669 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
1670 tab
.classList
.remove('cbi-tab');
1671 tab
.classList
.remove('cbi-tab-disabled');
1673 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
1676 group
.childNodes
.forEach(function(pane
) {
1677 if (L
.dom
.matches(pane
, '[data-tab]')) {
1678 if (pane
.getAttribute('data-tab') === name
) {
1679 pane
.setAttribute('data-tab-active', 'true');
1680 L
.ui
.tabs
.setActiveTabId(groupId
, index
);
1683 pane
.setAttribute('data-tab-active', 'false');
1693 changes
: L
.Class
.singleton({
1695 if (!L
.env
.sessionid
)
1698 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
1701 setIndicator: function(n
) {
1702 var i
= document
.querySelector('.uci_change_indicator');
1704 var poll
= document
.getElementById('xhr_poll_status');
1705 i
= poll
.parentNode
.insertBefore(E('a', {
1707 'class': 'uci_change_indicator label notice',
1708 'click': L
.bind(this.displayChanges
, this)
1713 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
1714 i
.classList
.add('flash');
1715 i
.style
.display
= '';
1718 i
.classList
.remove('flash');
1719 i
.style
.display
= 'none';
1723 renderChangeIndicator: function(changes
) {
1726 for (var config
in changes
)
1727 if (changes
.hasOwnProperty(config
))
1728 n_changes
+= changes
[config
].length
;
1730 this.changes
= changes
;
1731 this.setIndicator(n_changes
);
1735 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1736 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1737 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1738 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1739 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1740 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1741 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1742 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1743 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1744 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1747 displayChanges: function() {
1748 var list
= E('div', { 'class': 'uci-change-list' }),
1749 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
1750 E('div', { 'class': 'cbi-section' }, [
1751 E('strong', _('Legend:')),
1752 E('div', { 'class': 'uci-change-legend' }, [
1753 E('div', { 'class': 'uci-change-legend-label' }, [
1754 E('ins', ' '), ' ', _('Section added') ]),
1755 E('div', { 'class': 'uci-change-legend-label' }, [
1756 E('del', ' '), ' ', _('Section removed') ]),
1757 E('div', { 'class': 'uci-change-legend-label' }, [
1758 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1759 E('div', { 'class': 'uci-change-legend-label' }, [
1760 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1762 E('div', { 'class': 'right' }, [
1766 'click': L
.ui
.hideModal
,
1767 'value': _('Dismiss')
1771 'class': 'cbi-button cbi-button-positive important',
1772 'click': L
.bind(this.apply
, this, true),
1773 'value': _('Save & Apply')
1777 'class': 'cbi-button cbi-button-reset',
1778 'click': L
.bind(this.revert
, this),
1779 'value': _('Revert')
1783 for (var config
in this.changes
) {
1784 if (!this.changes
.hasOwnProperty(config
))
1787 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
1789 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
1790 var chg
= this.changes
[config
][i
],
1791 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
1793 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
1799 if (added
!= null && chg
[1] == added
[0])
1800 return '@' + added
[1] + '[-1]';
1805 return "'%h'".format(chg
[3].replace(/'/g, "'\"'\"'"));
1812 if (chg[0] == 'add')
1813 added = [ chg[1], chg[2] ];
1817 list.appendChild(E('br'));
1818 dlg.classList.add('uci-dialog');
1821 displayStatus: function(type, content) {
1823 var message = L.ui.showModal('', '');
1825 message.classList.add('alert-message');
1826 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1829 L.dom.content(message, content);
1831 if (!this.was_polling) {
1832 this.was_polling = L.Request.poll.active();
1833 L.Request.poll.stop();
1839 if (this.was_polling)
1840 L.Request.poll.start();
1844 rollback: function(checked) {
1846 this.displayStatus('warning spinning',
1847 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
1848 .format(L.env.apply_rollback)));
1850 var call = function(r, data, duration) {
1851 if (r.status === 204) {
1852 L.ui.changes.displayStatus('warning', [
1853 E('h4', _('Configuration has been rolled back!')),
1854 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)),
1855 E('div', { 'class': 'right' }, [
1859 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1860 'value': _('Dismiss')
1864 'class': 'btn cbi-button-action important',
1865 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1866 'value': _('Revert changes')
1870 'class': 'btn cbi-button-negative important',
1871 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1872 'value': _('Apply unchecked')
1880 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1881 window.setTimeout(function() {
1882 L.Request.request(L.url('admin/uci/confirm'), {
1884 timeout: L.env.apply_timeout * 1000,
1885 query: { sid: L.env.sessionid, token: L.env.token }
1890 call({ status: 0 });
1893 this.displayStatus('warning', [
1894 E('h4', _('Device unreachable!')),
1895 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.'))
1900 confirm: function(checked, deadline, override_token) {
1902 var ts = Date.now();
1904 this.displayStatus('notice');
1907 this.confirm_auth = { token: override_token };
1909 var call = function(r, data, duration) {
1910 if (Date.now() >= deadline) {
1911 window.clearTimeout(tt);
1912 L.ui.changes.rollback(checked);
1915 else if (r && (r.status === 200 || r.status === 204)) {
1916 document.dispatchEvent(new CustomEvent('uci-applied'));
1918 L.ui.changes.setIndicator(0);
1919 L.ui.changes.displayStatus('notice',
1920 E('p', _('Configuration has been applied.')));
1922 window.clearTimeout(tt);
1923 window.setTimeout(function() {
1924 //L.ui.changes.displayStatus(false);
1925 window.location = window.location.href.split('#')[0];
1926 }, L.env.apply_display * 1000);
1931 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1932 window.setTimeout(function() {
1933 L.Request.request(L.url('admin/uci/confirm'), {
1935 timeout: L.env.apply_timeout * 1000,
1936 query: L.ui.changes.confirm_auth
1937 }).then(call, call);
1941 var tick = function() {
1942 var now = Date.now();
1944 L.ui.changes.displayStatus('notice spinning',
1945 E('p', _('Waiting for configuration to get applied… %ds')
1946 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
1948 if (now >= deadline)
1951 tt = window.setTimeout(tick, 1000 - (now - ts));
1957 /* wait a few seconds for the settings to become effective */
1958 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
1961 apply: function(checked) {
1962 this.displayStatus('notice spinning',
1963 E('p', _('Starting configuration apply…')));
1965 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
1967 query: { sid: L.env.sessionid, token: L.env.token }
1968 }).then(function(r) {
1969 if (r.status === (checked ? 200 : 204)) {
1970 var tok = null; try { tok = r.json(); } catch(e) {}
1971 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
1972 L.ui.changes.confirm_auth = tok;
1974 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
1976 else if (checked && r.status === 204) {
1977 L.ui.changes.displayStatus('notice',
1978 E('p', _('There are no changes to apply')));
1980 window.setTimeout(function() {
1981 L.ui.changes.displayStatus(false);
1982 }, L.env.apply_display * 1000);
1985 L.ui.changes.displayStatus('warning',
1986 E('p', _('Apply request failed with status <code>%h</code>')
1987 .format(r.responseText || r.statusText || r.status)));
1989 window.setTimeout(function() {
1990 L.ui.changes.displayStatus(false);
1991 }, L.env.apply_display * 1000);
1996 revert: function() {
1997 this.displayStatus('notice spinning',
1998 E('p', _('Reverting configuration…')));
2000 L.Request.request(L.url('admin/uci/revert'), {
2002 query: { sid: L.env.sessionid, token: L.env.token }
2003 }).then(function(r) {
2004 if (r.status === 200) {
2005 document.dispatchEvent(new CustomEvent('uci-reverted'));
2007 L.ui.changes.setIndicator(0);
2008 L.ui.changes.displayStatus('notice',
2009 E('p', _('Changes have been reverted.')));
2011 window.setTimeout(function() {
2012 //L.ui.changes.displayStatus(false);
2013 window.location = window.location.href.split('#')[0];
2014 }, L.env.apply_display * 1000);
2017 L.ui.changes.displayStatus('warning',
2018 E('p', _('Revert request failed with status <code>%h</code>')
2019 .format(r.statusText || r.status)));
2021 window.setTimeout(function() {
2022 L.ui.changes.displayStatus(false);
2023 }, L.env.apply_display * 1000);
2029 addValidator: function(field, type, optional, vfunc /*, ... */) {
2033 var events = this.varargs(arguments, 3);
2034 if (events.length == 0)
2035 events.push('blur', 'keyup');
2038 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2039 validatorFn = cbiValidator.validate.bind(cbiValidator);
2041 for (var i = 0; i < events.length; i++)
2042 field.addEventListener(events[i], validatorFn);
2052 Textfield: UITextfield,
2053 Checkbox: UICheckbox,
2055 Dropdown: UIDropdown,
2056 DynamicList: UIDynamicList,
2057 Combobox: UICombobox,
2058 Hiddenfield: UIHiddenfield