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
)
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, 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"]'),
1332 for (var i
= 0; i
< items
.length
; i
++)
1333 v
.push(items
[i
].value
);
1338 setValue: function(values
) {
1339 if (!Array
.isArray(values
))
1340 values
= (values
!= null && values
!= '') ? [ values
] : [];
1342 var items
= this.node
.querySelectorAll('.item');
1344 for (var i
= 0; i
< items
.length
; i
++)
1345 if (items
[i
].parentNode
=== this.node
)
1346 this.removeItem(this.node
, items
[i
]);
1348 for (var i
= 0; i
< values
.length
; i
++)
1349 this.addItem(this.node
, values
[i
],
1350 this.choices
? this.choices
[values
[i
]] : null);
1354 var UIHiddenfield
= UIElement
.extend({
1355 __init__: function(value
, options
) {
1357 this.options
= Object
.assign({
1362 render: function() {
1363 var hiddenEl
= E('input', {
1364 'id': this.options
.id
,
1369 return this.bind(hiddenEl
);
1372 bind: function(hiddenEl
) {
1373 this.node
= hiddenEl
;
1375 L
.dom
.bindClassInstance(hiddenEl
, this);
1380 getValue: function() {
1381 return this.node
.value
;
1384 setValue: function(value
) {
1385 this.node
.value
= value
;
1390 return L
.Class
.extend({
1391 __init__: function() {
1392 modalDiv
= document
.body
.appendChild(
1393 L
.dom
.create('div', { id
: 'modal_overlay' },
1394 L
.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
1396 tooltipDiv
= document
.body
.appendChild(
1397 L
.dom
.create('div', { class: 'cbi-tooltip' }));
1399 /* setup old aliases */
1400 L
.showModal
= this.showModal
;
1401 L
.hideModal
= this.hideModal
;
1402 L
.showTooltip
= this.showTooltip
;
1403 L
.hideTooltip
= this.hideTooltip
;
1404 L
.itemlist
= this.itemlist
;
1406 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
1407 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
1408 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
1409 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
1411 document
.addEventListener('luci-loaded', this.tabs
.init
.bind(this.tabs
));
1412 document
.addEventListener('luci-loaded', this.changes
.init
.bind(this.changes
));
1413 document
.addEventListener('uci-loaded', this.changes
.init
.bind(this.changes
));
1417 showModal: function(title
, children
/* , ... */) {
1418 var dlg
= modalDiv
.firstElementChild
;
1420 dlg
.setAttribute('class', 'modal');
1422 for (var i
= 2; i
< arguments
.length
; i
++)
1423 dlg
.classList
.add(arguments
[i
]);
1425 L
.dom
.content(dlg
, L
.dom
.create('h4', {}, title
));
1426 L
.dom
.append(dlg
, children
);
1428 document
.body
.classList
.add('modal-overlay-active');
1433 hideModal: function() {
1434 document
.body
.classList
.remove('modal-overlay-active');
1438 showTooltip: function(ev
) {
1439 var target
= findParent(ev
.target
, '[data-tooltip]');
1444 if (tooltipTimeout
!== null) {
1445 window
.clearTimeout(tooltipTimeout
);
1446 tooltipTimeout
= null;
1449 var rect
= target
.getBoundingClientRect(),
1450 x
= rect
.left
+ window
.pageXOffset
,
1451 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
1453 tooltipDiv
.className
= 'cbi-tooltip';
1454 tooltipDiv
.innerHTML
= '▲ ';
1455 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
1457 if (target
.hasAttribute('data-tooltip-style'))
1458 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
1460 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
1461 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
1462 tooltipDiv
.firstChild
.data
= '▼ ' + tooltipDiv
.firstChild
.data
.substr(2);
1465 tooltipDiv
.style
.top
= y
+ 'px';
1466 tooltipDiv
.style
.left
= x
+ 'px';
1467 tooltipDiv
.style
.opacity
= 1;
1469 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
1471 detail
: { target
: target
}
1475 hideTooltip: function(ev
) {
1476 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
1477 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
1480 if (tooltipTimeout
!== null) {
1481 window
.clearTimeout(tooltipTimeout
);
1482 tooltipTimeout
= null;
1485 tooltipDiv
.style
.opacity
= 0;
1486 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
1488 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
1492 itemlist: function(node
, items
, separators
) {
1495 if (!Array
.isArray(separators
))
1496 separators
= [ separators
|| E('br') ];
1498 for (var i
= 0; i
< items
.length
; i
+= 2) {
1499 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
1500 var sep
= separators
[(i
/2) % separators
.length
],
1503 children
.push(E('span', { class: 'nowrap' }, [
1504 items
[i
] ? E('strong', items
[i
] + ': ') : '',
1508 if ((i
+2) < items
.length
)
1509 children
.push(L
.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
1513 L
.dom
.content(node
, children
);
1519 tabs
: L
.Class
.singleton({
1521 var groups
= [], prevGroup
= null, currGroup
= null;
1523 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
1524 var parent
= tab
.parentNode
;
1526 if (!parent
.hasAttribute('data-tab-group'))
1527 parent
.setAttribute('data-tab-group', groups
.length
);
1529 currGroup
= +parent
.getAttribute('data-tab-group');
1531 if (currGroup
!== prevGroup
) {
1532 prevGroup
= currGroup
;
1534 if (!groups
[currGroup
])
1535 groups
[currGroup
] = [];
1538 groups
[currGroup
].push(tab
);
1541 for (var i
= 0; i
< groups
.length
; i
++)
1542 this.initTabGroup(groups
[i
]);
1544 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
1549 this.setActiveTabId(-1, -1);
1552 initTabGroup: function(panes
) {
1553 if (typeof(panes
) != 'object' || !('length' in panes
) || panes
.length
=== 0)
1556 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
1557 group
= panes
[0].parentNode
,
1558 groupId
= +group
.getAttribute('data-tab-group'),
1561 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
1562 var name
= pane
.getAttribute('data-tab'),
1563 title
= pane
.getAttribute('data-tab-title'),
1564 active
= pane
.getAttribute('data-tab-active') === 'true';
1566 menu
.appendChild(E('li', {
1567 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
1571 'click': this.switchTab
.bind(this)
1578 group
.parentNode
.insertBefore(menu
, group
);
1580 if (selected
=== null) {
1581 selected
= this.getActiveTabId(groupId
);
1583 if (selected
< 0 || selected
>= panes
.length
)
1586 menu
.childNodes
[selected
].classList
.add('cbi-tab');
1587 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
1588 panes
[selected
].setAttribute('data-tab-active', 'true');
1590 this.setActiveTabId(groupId
, selected
);
1594 getActiveTabState: function() {
1595 var page
= document
.body
.getAttribute('data-page');
1598 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
1599 if (val
.page
=== page
&& Array
.isArray(val
.groups
))
1604 window
.sessionStorage
.removeItem('tab');
1605 return { page
: page
, groups
: [] };
1608 getActiveTabId: function(groupId
) {
1609 return +this.getActiveTabState().groups
[groupId
] || 0;
1612 setActiveTabId: function(groupId
, tabIndex
) {
1614 var state
= this.getActiveTabState();
1615 state
.groups
[groupId
] = tabIndex
;
1617 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
1619 catch (e
) { return false; }
1624 updateTabs: function(ev
) {
1625 document
.querySelectorAll('[data-tab-title]').forEach(function(pane
) {
1626 var menu
= pane
.parentNode
.previousElementSibling
,
1627 tab
= menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))),
1628 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
1630 if (!pane
.firstElementChild
) {
1631 tab
.style
.display
= 'none';
1632 tab
.classList
.remove('flash');
1634 else if (tab
.style
.display
=== 'none') {
1635 tab
.style
.display
= '';
1636 requestAnimationFrame(function() { tab
.classList
.add('flash') });
1640 tab
.setAttribute('data-errors', n_errors
);
1641 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
1642 tab
.setAttribute('data-tooltip-style', 'error');
1645 tab
.removeAttribute('data-errors');
1646 tab
.removeAttribute('data-tooltip');
1651 switchTab: function(ev
) {
1652 var tab
= ev
.target
.parentNode
,
1653 name
= tab
.getAttribute('data-tab'),
1654 menu
= tab
.parentNode
,
1655 group
= menu
.nextElementSibling
,
1656 groupId
= +group
.getAttribute('data-tab-group'),
1659 ev
.preventDefault();
1661 if (!tab
.classList
.contains('cbi-tab-disabled'))
1664 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
1665 tab
.classList
.remove('cbi-tab');
1666 tab
.classList
.remove('cbi-tab-disabled');
1668 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
1671 group
.childNodes
.forEach(function(pane
) {
1672 if (L
.dom
.matches(pane
, '[data-tab]')) {
1673 if (pane
.getAttribute('data-tab') === name
) {
1674 pane
.setAttribute('data-tab-active', 'true');
1675 L
.ui
.tabs
.setActiveTabId(groupId
, index
);
1678 pane
.setAttribute('data-tab-active', 'false');
1688 changes
: L
.Class
.singleton({
1690 if (!L
.env
.sessionid
)
1693 return uci
.changes().then(L
.bind(this.renderChangeIndicator
, this));
1696 setIndicator: function(n
) {
1697 var i
= document
.querySelector('.uci_change_indicator');
1699 var poll
= document
.getElementById('xhr_poll_status');
1700 i
= poll
.parentNode
.insertBefore(E('a', {
1702 'class': 'uci_change_indicator label notice',
1703 'click': L
.bind(this.displayChanges
, this)
1708 L
.dom
.content(i
, [ _('Unsaved Changes'), ': ', n
]);
1709 i
.classList
.add('flash');
1710 i
.style
.display
= '';
1713 i
.classList
.remove('flash');
1714 i
.style
.display
= 'none';
1718 renderChangeIndicator: function(changes
) {
1721 for (var config
in changes
)
1722 if (changes
.hasOwnProperty(config
))
1723 n_changes
+= changes
[config
].length
;
1725 this.changes
= changes
;
1726 this.setIndicator(n_changes
);
1730 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1731 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1732 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1733 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1734 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1735 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1736 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1737 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1738 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1739 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1742 displayChanges: function() {
1743 var list
= E('div', { 'class': 'uci-change-list' }),
1744 dlg
= L
.ui
.showModal(_('Configuration') + ' / ' + _('Changes'), [
1745 E('div', { 'class': 'cbi-section' }, [
1746 E('strong', _('Legend:')),
1747 E('div', { 'class': 'uci-change-legend' }, [
1748 E('div', { 'class': 'uci-change-legend-label' }, [
1749 E('ins', ' '), ' ', _('Section added') ]),
1750 E('div', { 'class': 'uci-change-legend-label' }, [
1751 E('del', ' '), ' ', _('Section removed') ]),
1752 E('div', { 'class': 'uci-change-legend-label' }, [
1753 E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
1754 E('div', { 'class': 'uci-change-legend-label' }, [
1755 E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
1757 E('div', { 'class': 'right' }, [
1761 'click': L
.ui
.hideModal
,
1762 'value': _('Dismiss')
1766 'class': 'cbi-button cbi-button-positive important',
1767 'click': L
.bind(this.apply
, this, true),
1768 'value': _('Save & Apply')
1772 'class': 'cbi-button cbi-button-reset',
1773 'click': L
.bind(this.revert
, this),
1774 'value': _('Revert')
1778 for (var config
in this.changes
) {
1779 if (!this.changes
.hasOwnProperty(config
))
1782 list
.appendChild(E('h5', '# /etc/config/%s'.format(config
)));
1784 for (var i
= 0, added
= null; i
< this.changes
[config
].length
; i
++) {
1785 var chg
= this.changes
[config
][i
],
1786 tpl
= this.changeTemplates
['%s-%d'.format(chg
[0], chg
.length
)];
1788 list
.appendChild(E(tpl
.replace(/%([01234])/g, function(m0
, m1
) {
1794 if (added
!= null && chg
[1] == added
[0])
1795 return '@' + added
[1] + '[-1]';
1800 return "'" + chg
[3].replace(/'/g, "'\"'\"'") + "'";
1807 if (chg[0] == 'add
')
1808 added = [ chg[1], chg[2] ];
1812 list.appendChild(E('br
'));
1813 dlg.classList.add('uci
-dialog
');
1816 displayStatus: function(type, content) {
1818 var message = L.ui.showModal('', '');
1820 message.classList.add('alert
-message
');
1821 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1824 L.dom.content(message, content);
1826 if (!this.was_polling) {
1827 this.was_polling = L.Request.poll.active();
1828 L.Request.poll.stop();
1834 if (this.was_polling)
1835 L.Request.poll.start();
1839 rollback: function(checked) {
1841 this.displayStatus('warning spinning
',
1842 E('p
', _('Failed to confirm apply within
%ds
, waiting
for rollback
…')
1843 .format(L.env.apply_rollback)));
1845 var call = function(r, data, duration) {
1846 if (r.status === 204) {
1847 L.ui.changes.displayStatus('warning
', [
1848 E('h4
', _('Configuration has been rolled back
!')),
1849 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)),
1850 E('div
', { 'class': 'right
' }, [
1854 'click
': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1855 'value
': _('Dismiss
')
1859 'class': 'btn cbi
-button
-action important
',
1860 'click
': L.bind(L.ui.changes.revert, L.ui.changes),
1861 'value
': _('Revert changes
')
1865 'class': 'btn cbi
-button
-negative important
',
1866 'click
': L.bind(L.ui.changes.apply, L.ui.changes, false),
1867 'value
': _('Apply unchecked
')
1875 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1876 window.setTimeout(function() {
1877 L.Request.request(L.url('admin
/uci/confirm'), {
1879 timeout: L.env.apply_timeout * 1000,
1880 query: { sid: L.env.sessionid, token: L.env.token }
1885 call({ status: 0 });
1888 this.displayStatus('warning
', [
1889 E('h4
', _('Device unreachable
!')),
1890 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
.'))
1895 confirm: function(checked, deadline, override_token) {
1897 var ts = Date.now();
1899 this.displayStatus('notice
');
1902 this.confirm_auth = { token: override_token };
1904 var call = function(r, data, duration) {
1905 if (Date.now() >= deadline) {
1906 window.clearTimeout(tt);
1907 L.ui.changes.rollback(checked);
1910 else if (r && (r.status === 200 || r.status === 204)) {
1911 document.dispatchEvent(new CustomEvent('uci
-applied
'));
1913 L.ui.changes.setIndicator(0);
1914 L.ui.changes.displayStatus('notice
',
1915 E('p
', _('Configuration has been applied
.')));
1917 window.clearTimeout(tt);
1918 window.setTimeout(function() {
1919 //L.ui.changes.displayStatus(false);
1920 window.location = window.location.href.split('#')[0];
1921 }, L.env.apply_display * 1000);
1926 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1927 window.setTimeout(function() {
1928 L.Request.request(L.url('admin
/uci/confirm'), {
1930 timeout: L.env.apply_timeout * 1000,
1931 query: L.ui.changes.confirm_auth
1936 var tick = function() {
1937 var now = Date.now();
1939 L.ui.changes.displayStatus('notice spinning
',
1940 E('p
', _('Waiting
for configuration to
get applied
… %ds
')
1941 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
1943 if (now >= deadline)
1946 tt = window.setTimeout(tick, 1000 - (now - ts));
1952 /* wait a few seconds for the settings to become effective */
1953 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
1956 apply: function(checked) {
1957 this.displayStatus('notice spinning
',
1958 E('p
', _('Starting configuration apply
…')));
1960 L.Request.request(L.url('admin
/uci
', checked ? 'apply_rollback
' : 'apply_unchecked
'), {
1962 query: { sid: L.env.sessionid, token: L.env.token }
1963 }).then(function(r) {
1964 if (r.status === (checked ? 200 : 204)) {
1965 var tok = null; try { tok = r.json(); } catch(e) {}
1966 if (checked && tok !== null && typeof(tok) === 'object
' && typeof(tok.token) === 'string
')
1967 L.ui.changes.confirm_auth = tok;
1969 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
1971 else if (checked && r.status === 204) {
1972 L.ui.changes.displayStatus('notice
',
1973 E('p
', _('There are no changes to apply
')));
1975 window.setTimeout(function() {
1976 L.ui.changes.displayStatus(false);
1977 }, L.env.apply_display * 1000);
1980 L.ui.changes.displayStatus('warning
',
1981 E('p
', _('Apply request failed
with status
<code
>%h
</code
>')
1982 .format(r.responseText || r.statusText || r.status)));
1984 window.setTimeout(function() {
1985 L.ui.changes.displayStatus(false);
1986 }, L.env.apply_display * 1000);
1991 revert: function() {
1992 this.displayStatus('notice spinning
',
1993 E('p
', _('Reverting configuration
…')));
1995 L.Request.request(L.url('admin
/uci/revert
'), {
1997 query: { sid: L.env.sessionid, token: L.env.token }
1998 }).then(function(r) {
1999 if (r.status === 200) {
2000 document.dispatchEvent(new CustomEvent('uci
-reverted
'));
2002 L.ui.changes.setIndicator(0);
2003 L.ui.changes.displayStatus('notice
',
2004 E('p
', _('Changes have been reverted
.')));
2006 window.setTimeout(function() {
2007 //L.ui.changes.displayStatus(false);
2008 window.location = window.location.href.split('#')[0];
2009 }, L.env.apply_display * 1000);
2012 L.ui.changes.displayStatus('warning
',
2013 E('p
', _('Revert request failed
with status
<code
>%h
</code
>')
2014 .format(r.statusText || r.status)));
2016 window.setTimeout(function() {
2017 L.ui.changes.displayStatus(false);
2018 }, L.env.apply_display * 1000);
2024 addValidator: function(field, type, optional, vfunc /*, ... */) {
2028 var events = this.varargs(arguments, 3);
2029 if (events.length == 0)
2030 events.push('blur
', 'keyup
');
2033 var cbiValidator = L.validation.create(field, type, optional, vfunc),
2034 validatorFn = cbiValidator.validate.bind(cbiValidator);
2036 for (var i = 0; i < events.length; i++)
2037 field.addEventListener(events[i], validatorFn);
2047 Textfield: UITextfield,
2048 Checkbox: UICheckbox,
2050 Dropdown: UIDropdown,
2051 DynamicList: UIDynamicList,
2052 Combobox: UICombobox,
2053 Hiddenfield: UIHiddenfield