1 (function(window
, document
, undefined) {
2 /* Object.assign polyfill for IE */
3 if (typeof Object
.assign
!== 'function') {
4 Object
.defineProperty(Object
, 'assign', {
5 value
: function assign(target
, varArgs
) {
7 throw new TypeError('Cannot convert undefined or null to object');
9 var to
= Object(target
);
11 for (var index
= 1; index
< arguments
.length
; index
++)
12 if (arguments
[index
] != null)
13 for (var nextKey
in arguments
[index
])
14 if (Object
.prototype.hasOwnProperty
.call(arguments
[index
], nextKey
))
15 to
[nextKey
] = arguments
[index
][nextKey
];
25 * Class declaration and inheritance helper
28 var toCamelCase = function(s
) {
29 return s
.replace(/(?:^|[\. -])(.)/g, function(m0
, m1
) { return m1
.toUpperCase() });
32 var superContext
= null, Class
= Object
.assign(function() {}, {
33 extend: function(properties
) {
35 __base__
: { value
: this.prototype },
36 __name__
: { value
: properties
.__name__
|| 'anonymous' }
39 var ClassConstructor = function() {
40 if (!(this instanceof ClassConstructor
))
41 throw new TypeError('Constructor must not be called without "new"');
43 if (Object
.getPrototypeOf(this).hasOwnProperty('__init__')) {
44 if (typeof(this.__init__
) != 'function')
45 throw new TypeError('Class __init__ member is not a function');
47 this.__init__
.apply(this, arguments
)
50 this.super('__init__', arguments
);
54 for (var key
in properties
)
55 if (!props
[key
] && properties
.hasOwnProperty(key
))
56 props
[key
] = { value
: properties
[key
], writable
: true };
58 ClassConstructor
.prototype = Object
.create(this.prototype, props
);
59 ClassConstructor
.prototype.constructor = ClassConstructor
;
60 Object
.assign(ClassConstructor
, this);
61 ClassConstructor
.displayName
= toCamelCase(props
.__name__
.value
+ 'Class');
63 return ClassConstructor
;
66 singleton: function(properties
/*, ... */) {
67 return Class
.extend(properties
)
68 .instantiate(Class
.prototype.varargs(arguments
, 1));
71 instantiate: function(args
) {
72 return new (Function
.prototype.bind
.apply(this,
73 Class
.prototype.varargs(args
, 0, null)))();
76 call: function(self
, method
) {
77 if (typeof(this.prototype[method
]) != 'function')
78 throw new ReferenceError(method
+ ' is not defined in class');
80 return this.prototype[method
].apply(self
, self
.varargs(arguments
, 1));
83 isSubclass: function(_class
) {
84 return (_class
!= null &&
85 typeof(_class
) == 'function' &&
86 _class
.prototype instanceof this);
90 varargs: function(args
, offset
/*, ... */) {
91 return Array
.prototype.slice
.call(arguments
, 2)
92 .concat(Array
.prototype.slice
.call(args
, offset
));
95 super: function(key
, callArgs
) {
96 for (superContext
= Object
.getPrototypeOf(superContext
||
97 Object
.getPrototypeOf(this));
98 superContext
&& !superContext
.hasOwnProperty(key
);
99 superContext
= Object
.getPrototypeOf(superContext
)) { }
104 var res
= superContext
[key
];
106 if (arguments
.length
> 1) {
107 if (typeof(res
) != 'function')
108 throw new ReferenceError(key
+ ' is not a function in base class');
110 if (typeof(callArgs
) != 'object')
111 callArgs
= this.varargs(arguments
, 1);
113 res
= res
.apply(this, callArgs
);
121 toString: function() {
122 var s
= '[' + this.constructor.displayName
+ ']', f
= true;
123 for (var k
in this) {
124 if (this.hasOwnProperty(k
)) {
125 s
+= (f
? ' {\n' : '') + ' ' + k
+ ': ' + typeof(this[k
]) + '\n';
129 return s
+ (f
? '' : '}');
136 * HTTP Request helper
139 Headers
= Class
.extend({
140 __name__
: 'LuCI.XHR.Headers',
141 __init__: function(xhr
) {
142 var hdrs
= this.headers
= {};
143 xhr
.getAllResponseHeaders().split(/\r\n/).forEach(function(line
) {
144 var m
= /^([^:]+):(.*)$/.exec(line
);
146 hdrs
[m
[1].trim().toLowerCase()] = m
[2].trim();
150 has: function(name
) {
151 return this.headers
.hasOwnProperty(String(name
).toLowerCase());
154 get: function(name
) {
155 var key
= String(name
).toLowerCase();
156 return this.headers
.hasOwnProperty(key
) ? this.headers
[key
] : null;
160 Response
= Class
.extend({
161 __name__
: 'LuCI.XHR.Response',
162 __init__: function(xhr
, url
, duration
) {
163 this.ok
= (xhr
.status
>= 200 && xhr
.status
<= 299);
164 this.status
= xhr
.status
;
165 this.statusText
= xhr
.statusText
;
166 this.responseText
= xhr
.responseText
;
167 this.headers
= new Headers(xhr
);
168 this.duration
= duration
;
174 return JSON
.parse(this.responseText
);
178 return this.responseText
;
182 Request
= Class
.singleton({
183 __name__
: 'LuCI.Request',
187 request: function(target
, options
) {
188 var state
= { xhr
: new XMLHttpRequest(), url
: target
, start
: Date
.now() },
189 opt
= Object
.assign({}, options
, state
),
192 callback
= this.handleReadyStateChange
;
194 return new Promise(function(resolveFn
, rejectFn
) {
195 opt
.xhr
.onreadystatechange
= callback
.bind(opt
, resolveFn
, rejectFn
);
196 opt
.method
= String(opt
.method
|| 'GET').toUpperCase();
198 if ('query' in opt
) {
199 var q
= (opt
.query
!= null) ? Object
.keys(opt
.query
).map(function(k
) {
200 if (opt
.query
[k
] != null) {
201 var v
= (typeof(opt
.query
[k
]) == 'object')
202 ? JSON
.stringify(opt
.query
[k
])
203 : String(opt
.query
[k
]);
205 return '%s=%s'.format(encodeURIComponent(k
), encodeURIComponent(v
));
208 return encodeURIComponent(k
);
213 switch (opt
.method
) {
217 opt
.url
+= ((/\?/).test(opt
.url
) ? '&' : '?') + q
;
221 if (content
== null) {
223 contenttype
= 'application/x-www-form-urlencoded';
230 opt
.url
+= ((/\?/).test(opt
.url
) ? '&' : '?') + (new Date()).getTime();
232 if (!/^(?:[^/]+:)?\/\//.test(opt
.url
))
233 opt
.url
= location
.protocol
+ '//' + location
.host
+ opt
.url
;
235 if ('username' in opt
&& 'password' in opt
)
236 opt
.xhr
.open(opt
.method
, opt
.url
, true, opt
.username
, opt
.password
);
238 opt
.xhr
.open(opt
.method
, opt
.url
, true);
240 opt
.xhr
.responseType
= 'text';
241 opt
.xhr
.overrideMimeType('application/octet-stream');
243 if ('timeout' in opt
)
244 opt
.xhr
.timeout
= +opt
.timeout
;
246 if ('credentials' in opt
)
247 opt
.xhr
.withCredentials
= !!opt
.credentials
;
249 if (opt
.content
!= null) {
250 switch (typeof(opt
.content
)) {
252 content
= opt
.content(xhr
);
256 content
= JSON
.stringify(opt
.content
);
257 contenttype
= 'application/json';
261 content
= String(opt
.content
);
265 if ('headers' in opt
)
266 for (var header
in opt
.headers
)
267 if (opt
.headers
.hasOwnProperty(header
)) {
268 if (header
.toLowerCase() != 'content-type')
269 opt
.xhr
.setRequestHeader(header
, opt
.headers
[header
]);
271 contenttype
= opt
.headers
[header
];
274 if (contenttype
!= null)
275 opt
.xhr
.setRequestHeader('Content-Type', contenttype
);
278 opt
.xhr
.send(content
);
281 rejectFn
.call(opt
, e
);
286 handleReadyStateChange: function(resolveFn
, rejectFn
, ev
) {
289 if (xhr
.readyState
!== 4)
292 if (xhr
.status
=== 0 && xhr
.statusText
=== '') {
293 rejectFn
.call(this, new Error('XHR request aborted by browser'));
296 var response
= new Response(
297 xhr
, xhr
.responseURL
|| this.url
, Date
.now() - this.start
);
299 Promise
.all(Request
.interceptors
.map(function(fn
) { return fn(response
) }))
300 .then(resolveFn
.bind(this, response
))
301 .catch(rejectFn
.bind(this));
310 get: function(url
, options
) {
311 return this.request(url
, Object
.assign({ method
: 'GET' }, options
));
314 post: function(url
, data
, options
) {
315 return this.request(url
, Object
.assign({ method
: 'POST', content
: data
}, options
));
318 addInterceptor: function(interceptorFn
) {
319 if (typeof(interceptorFn
) == 'function')
320 this.interceptors
.push(interceptorFn
);
321 return interceptorFn
;
324 removeInterceptor: function(interceptorFn
) {
325 var oldlen
= this.interceptors
.length
, i
= oldlen
;
327 if (this.interceptors
[i
] === interceptorFn
)
328 this.interceptors
.splice(i
, 1);
329 return (this.interceptors
.length
< oldlen
);
332 poll
: Class
.singleton({
333 __name__
: 'LuCI.Request.Poll',
337 add: function(interval
, url
, options
, callback
) {
338 if (isNaN(interval
) || interval
<= 0)
339 throw new TypeError('Invalid poll interval');
352 remove: function(entry
) {
353 var oldlen
= this.queue
.length
, i
= oldlen
;
356 if (this.queue
[i
] === entry
) {
357 delete this.queue
[i
].running
;
358 this.queue
.splice(i
, 1);
361 if (!this.queue
.length
)
364 return (this.queue
.length
< oldlen
);
368 if (!this.queue
.length
|| this.active())
372 this.timer
= window
.setInterval(this.step
, 1000);
374 document
.dispatchEvent(new CustomEvent('poll-start'));
382 document
.dispatchEvent(new CustomEvent('poll-stop'));
383 window
.clearInterval(this.timer
);
390 Request
.poll
.queue
.forEach(function(e
) {
391 if ((Request
.poll
.tick
% e
.interval
) != 0)
397 var opts
= Object
.assign({}, e
.options
,
398 { timeout
: e
.interval
* 1000 - 5 });
401 Request
.request(e
.url
, opts
)
402 .then(function(res
) {
403 if (!e
.running
|| !Request
.poll
.active())
407 e
.callback(res
, res
.json(), res
.duration
);
410 e
.callback(res
, null, res
.duration
);
413 .finally(function() { delete e
.running
});
416 Request
.poll
.tick
= (Request
.poll
.tick
+ 1) % Math
.pow(2, 32);
420 return (this.timer
!= null);
428 tooltipTimeout
= null,
431 originalCBIInit
= null,
434 LuCI
= Class
.extend({
436 __init__: function(env
) {
437 Object
.assign(this.env
, env
);
439 modalDiv
= document
.body
.appendChild(
440 this.dom
.create('div', { id
: 'modal_overlay' },
441 this.dom
.create('div', { class: 'modal', role
: 'dialog', 'aria-modal': true })));
443 tooltipDiv
= document
.body
.appendChild(this.dom
.create('div', { class: 'cbi-tooltip' }));
445 document
.addEventListener('mouseover', this.showTooltip
.bind(this), true);
446 document
.addEventListener('mouseout', this.hideTooltip
.bind(this), true);
447 document
.addEventListener('focus', this.showTooltip
.bind(this), true);
448 document
.addEventListener('blur', this.hideTooltip
.bind(this), true);
450 document
.addEventListener('DOMContentLoaded', this.setupDOM
.bind(this));
452 document
.addEventListener('poll-start', function(ev
) {
453 document
.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e
) {
454 e
.style
.display
= (e
.id
== 'xhr_poll_status_off') ? 'none' : '';
458 document
.addEventListener('poll-stop', function(ev
) {
459 document
.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e
) {
460 e
.style
.display
= (e
.id
== 'xhr_poll_status_on') ? 'none' : '';
464 originalCBIInit
= window
.cbi_init
;
465 window
.cbi_init = function() {};
469 require: function(name
, from) {
470 var L
= this, url
= null, from = from || [];
472 /* Class already loaded */
473 if (classes
[name
] != null) {
474 /* Circular dependency */
475 if (from.indexOf(name
) != -1)
476 throw new Error('Circular dependency: class "%s" depends on "%s"'
477 .format(name
, from.join('" which depends on "')));
479 return classes
[name
];
482 document
.querySelectorAll('script[src$="/luci.js"]').forEach(function(s
) {
483 url
= '%s/%s.js'.format(
484 s
.getAttribute('src').replace(/\/luci\.js$/, ''),
485 name
.replace(/\./g, '/'));
489 throw new Error('Cannot find url of luci.js');
491 from = [ name
].concat(from);
493 var compileClass = function(res
) {
495 throw new Error('HTTP error %d while loading class file "%s"'
496 .format(res
.status
, url
));
498 var source
= res
.text(),
499 reqmatch
= /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
503 /* find require statements in source */
504 for (var m
= reqmatch
.exec(source
); m
; m
= reqmatch
.exec(source
)) {
505 var dep
= m
[1], as
= m
[2] || dep
.replace(/[^a-zA-Z0-9_]/g, '_');
506 depends
.push(L
.require(dep
, from));
510 /* load dependencies and instantiate class */
511 return Promise
.all(depends
).then(function(instances
) {
514 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
515 .format(args
, source
, res
.url
));
518 throw new SyntaxError('%s\n in %s:%s'
519 .format(error
.message
, res
.url
, error
.lineNumber
|| '?'));
522 _factory
.displayName
= toCamelCase(name
+ 'ClassFactory');
523 _class
= _factory
.apply(_factory
, [window
, document
, L
].concat(instances
));
525 if (!Class
.isSubclass(_class
))
526 throw new TypeError('"%s" factory yields invalid constructor'
529 if (_class
.displayName
== 'AnonymousClass')
530 _class
.displayName
= toCamelCase(name
+ 'Class');
532 var ptr
= Object
.getPrototypeOf(L
),
533 parts
= name
.split(/\./),
534 instance
= new _class();
536 for (var i
= 0; ptr
&& i
< parts
.length
- 1; i
++)
540 throw new Error('Parent "%s" for class "%s" is missing'
541 .format(parts
.slice(0, i
).join('.'), name
));
543 classes
[name
] = ptr
[parts
[i
]] = instance
;
549 /* Request class file */
550 classes
[name
] = Request
.get(url
, { cache
: true }).then(compileClass
);
552 return classes
[name
];
556 setupDOM: function(ev
) {
559 Request
.addInterceptor(function(res
) {
560 if (res
.status
!= 403 || res
.headers
.get('X-LuCI-Login-Required') != 'yes')
565 L
.showModal(_('Session expired'), [
566 E('div', { class: 'alert-message warning' },
567 _('A new login is required since the authentication session expired.')),
568 E('div', { class: 'right' },
570 class: 'btn primary',
572 var loc
= window
.location
;
573 window
.location
= loc
.protocol
+ '//' + loc
.host
+ loc
.pathname
+ loc
.search
;
575 }, _('To login…')))
578 return Promise
.reject(new Error('Session expired'));
582 Request
.poll
.start();
587 /* URL construction helpers */
588 path: function(prefix
, parts
) {
589 var url
= [ prefix
|| '' ];
591 for (var i
= 0; i
< parts
.length
; i
++)
592 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts
[i
]))
593 url
.push('/', parts
[i
]);
595 if (url
.length
=== 1)
602 return this.path(this.env
.scriptname
, arguments
);
605 resource: function() {
606 return this.path(this.env
.resource
, arguments
);
609 location: function() {
610 return this.path(this.env
.scriptname
, this.env
.requestpath
);
614 /* HTTP resource fetching */
615 get: function(url
, args
, cb
) {
616 return this.poll(null, url
, args
, cb
, false);
619 post: function(url
, args
, cb
) {
620 return this.poll(null, url
, args
, cb
, true);
623 poll: function(interval
, url
, args
, cb
, post
) {
624 if (interval
!== null && interval
<= 0)
625 interval
= this.env
.pollinterval
;
627 var data
= post
? { token
: this.env
.token
} : null,
628 method
= post
? 'POST' : 'GET';
630 if (!/^(?:\/|\S+:\/\/)/.test(url
))
634 data
= Object
.assign(data
|| {}, args
);
636 if (interval
!== null)
637 return Request
.poll
.add(interval
, url
, { method
: method
, query
: data
}, cb
);
639 return Request
.request(url
, { method
: method
, query
: data
})
640 .then(function(res
) {
642 if (/^application\/json\b/.test(res
.headers
.get('Content-Type')))
643 try { json
= res
.json() } catch(e
) {}
644 cb(res
.xhr
, json
, res
.duration
);
648 stop: function(entry
) { return Request
.poll
.remove(entry
) },
649 halt: function() { return Request
.poll
.stop() },
650 run: function() { return Request
.poll
.start() },
654 showModal: function(title
, children
) {
655 var dlg
= modalDiv
.firstElementChild
;
657 dlg
.setAttribute('class', 'modal');
659 this.dom
.content(dlg
, this.dom
.create('h4', {}, title
));
660 this.dom
.append(dlg
, children
);
662 document
.body
.classList
.add('modal-overlay-active');
667 hideModal: function() {
668 document
.body
.classList
.remove('modal-overlay-active');
673 showTooltip: function(ev
) {
674 var target
= findParent(ev
.target
, '[data-tooltip]');
679 if (tooltipTimeout
!== null) {
680 window
.clearTimeout(tooltipTimeout
);
681 tooltipTimeout
= null;
684 var rect
= target
.getBoundingClientRect(),
685 x
= rect
.left
+ window
.pageXOffset
,
686 y
= rect
.top
+ rect
.height
+ window
.pageYOffset
;
688 tooltipDiv
.className
= 'cbi-tooltip';
689 tooltipDiv
.innerHTML
= 'â–² ';
690 tooltipDiv
.firstChild
.data
+= target
.getAttribute('data-tooltip');
692 if (target
.hasAttribute('data-tooltip-style'))
693 tooltipDiv
.classList
.add(target
.getAttribute('data-tooltip-style'));
695 if ((y
+ tooltipDiv
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
)) {
696 y
-= (tooltipDiv
.offsetHeight
+ target
.offsetHeight
);
697 tooltipDiv
.firstChild
.data
= 'â–¼ ' + tooltipDiv
.firstChild
.data
.substr(2);
700 tooltipDiv
.style
.top
= y
+ 'px';
701 tooltipDiv
.style
.left
= x
+ 'px';
702 tooltipDiv
.style
.opacity
= 1;
704 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-open', {
706 detail
: { target
: target
}
710 hideTooltip: function(ev
) {
711 if (ev
.target
=== tooltipDiv
|| ev
.relatedTarget
=== tooltipDiv
||
712 tooltipDiv
.contains(ev
.target
) || tooltipDiv
.contains(ev
.relatedTarget
))
715 if (tooltipTimeout
!== null) {
716 window
.clearTimeout(tooltipTimeout
);
717 tooltipTimeout
= null;
720 tooltipDiv
.style
.opacity
= 0;
721 tooltipTimeout
= window
.setTimeout(function() { tooltipDiv
.removeAttribute('style'); }, 250);
723 tooltipDiv
.dispatchEvent(new CustomEvent('tooltip-close', { bubbles
: true }));
728 itemlist: function(node
, items
, separators
) {
731 if (!Array
.isArray(separators
))
732 separators
= [ separators
|| E('br') ];
734 for (var i
= 0; i
< items
.length
; i
+= 2) {
735 if (items
[i
+1] !== null && items
[i
+1] !== undefined) {
736 var sep
= separators
[(i
/2) % separators
.length
],
739 children
.push(E('span', { class: 'nowrap' }, [
740 items
[i
] ? E('strong', items
[i
] + ': ') : '',
744 if ((i
+2) < items
.length
)
745 children
.push(this.dom
.elem(sep
) ? sep
.cloneNode(true) : sep
);
749 this.dom
.content(node
, children
);
759 LuCI
.prototype.tabs
= {
761 var groups
= [], prevGroup
= null, currGroup
= null;
763 document
.querySelectorAll('[data-tab]').forEach(function(tab
) {
764 var parent
= tab
.parentNode
;
766 if (!parent
.hasAttribute('data-tab-group'))
767 parent
.setAttribute('data-tab-group', groups
.length
);
769 currGroup
= +parent
.getAttribute('data-tab-group');
771 if (currGroup
!== prevGroup
) {
772 prevGroup
= currGroup
;
774 if (!groups
[currGroup
])
775 groups
[currGroup
] = [];
778 groups
[currGroup
].push(tab
);
781 for (var i
= 0; i
< groups
.length
; i
++)
782 this.initTabGroup(groups
[i
]);
784 document
.addEventListener('dependency-update', this.updateTabs
.bind(this));
789 this.setActiveTabId(-1, -1);
792 initTabGroup: function(panes
) {
793 if (!Array
.isArray(panes
) || panes
.length
=== 0)
796 var menu
= E('ul', { 'class': 'cbi-tabmenu' }),
797 group
= panes
[0].parentNode
,
798 groupId
= +group
.getAttribute('data-tab-group'),
801 for (var i
= 0, pane
; pane
= panes
[i
]; i
++) {
802 var name
= pane
.getAttribute('data-tab'),
803 title
= pane
.getAttribute('data-tab-title'),
804 active
= pane
.getAttribute('data-tab-active') === 'true';
806 menu
.appendChild(E('li', {
807 'class': active
? 'cbi-tab' : 'cbi-tab-disabled',
811 'click': this.switchTab
.bind(this)
818 group
.parentNode
.insertBefore(menu
, group
);
820 if (selected
=== null) {
821 selected
= this.getActiveTabId(groupId
);
823 if (selected
< 0 || selected
>= panes
.length
)
826 menu
.childNodes
[selected
].classList
.add('cbi-tab');
827 menu
.childNodes
[selected
].classList
.remove('cbi-tab-disabled');
828 panes
[selected
].setAttribute('data-tab-active', 'true');
830 this.setActiveTabId(groupId
, selected
);
834 getActiveTabState: function() {
835 var page
= document
.body
.getAttribute('data-page');
838 var val
= JSON
.parse(window
.sessionStorage
.getItem('tab'));
839 if (val
.page
=== page
&& Array
.isArray(val
.groups
))
844 window
.sessionStorage
.removeItem('tab');
845 return { page
: page
, groups
: [] };
848 getActiveTabId: function(groupId
) {
849 return +this.getActiveTabState().groups
[groupId
] || 0;
852 setActiveTabId: function(groupId
, tabIndex
) {
854 var state
= this.getActiveTabState();
855 state
.groups
[groupId
] = tabIndex
;
857 window
.sessionStorage
.setItem('tab', JSON
.stringify(state
));
859 catch (e
) { return false; }
864 updateTabs: function(ev
) {
865 document
.querySelectorAll('[data-tab-title]').forEach(function(pane
) {
866 var menu
= pane
.parentNode
.previousElementSibling
,
867 tab
= menu
.querySelector('[data-tab="%s"]'.format(pane
.getAttribute('data-tab'))),
868 n_errors
= pane
.querySelectorAll('.cbi-input-invalid').length
;
870 if (!pane
.firstElementChild
) {
871 tab
.style
.display
= 'none';
872 tab
.classList
.remove('flash');
874 else if (tab
.style
.display
=== 'none') {
875 tab
.style
.display
= '';
876 requestAnimationFrame(function() { tab
.classList
.add('flash') });
880 tab
.setAttribute('data-errors', n_errors
);
881 tab
.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors
));
882 tab
.setAttribute('data-tooltip-style', 'error');
885 tab
.removeAttribute('data-errors');
886 tab
.removeAttribute('data-tooltip');
891 switchTab: function(ev
) {
892 var tab
= ev
.target
.parentNode
,
893 name
= tab
.getAttribute('data-tab'),
894 menu
= tab
.parentNode
,
895 group
= menu
.nextElementSibling
,
896 groupId
= +group
.getAttribute('data-tab-group'),
901 if (!tab
.classList
.contains('cbi-tab-disabled'))
904 menu
.querySelectorAll('[data-tab]').forEach(function(tab
) {
905 tab
.classList
.remove('cbi-tab');
906 tab
.classList
.remove('cbi-tab-disabled');
908 tab
.getAttribute('data-tab') === name
? 'cbi-tab' : 'cbi-tab-disabled');
911 group
.childNodes
.forEach(function(pane
) {
912 if (L
.dom
.matches(pane
, '[data-tab]')) {
913 if (pane
.getAttribute('data-tab') === name
) {
914 pane
.setAttribute('data-tab-active', 'true');
915 L
.tabs
.setActiveTabId(groupId
, index
);
918 pane
.setAttribute('data-tab-active', 'false');
927 /* DOM manipulation */
928 LuCI
.prototype.dom
= {
930 return (typeof(e
) === 'object' && e
!== null && 'nodeType' in e
);
937 domParser
= domParser
|| new DOMParser();
938 elem
= domParser
.parseFromString(s
, 'text/html').body
.firstChild
;
944 dummyElem
= dummyElem
|| document
.createElement('div');
945 dummyElem
.innerHTML
= s
;
946 elem
= dummyElem
.firstChild
;
954 matches: function(node
, selector
) {
955 var m
= this.elem(node
) ? node
.matches
|| node
.msMatchesSelector
: null;
956 return m
? m
.call(node
, selector
) : false;
959 parent: function(node
, selector
) {
960 if (this.elem(node
) && node
.closest
)
961 return node
.closest(selector
);
963 while (this.elem(node
))
964 if (this.matches(node
, selector
))
967 node
= node
.parentNode
;
972 append: function(node
, children
) {
973 if (!this.elem(node
))
976 if (Array
.isArray(children
)) {
977 for (var i
= 0; i
< children
.length
; i
++)
978 if (this.elem(children
[i
]))
979 node
.appendChild(children
[i
]);
980 else if (children
!== null && children
!== undefined)
981 node
.appendChild(document
.createTextNode('' + children
[i
]));
983 return node
.lastChild
;
985 else if (typeof(children
) === 'function') {
986 return this.append(node
, children(node
));
988 else if (this.elem(children
)) {
989 return node
.appendChild(children
);
991 else if (children
!== null && children
!== undefined) {
992 node
.innerHTML
= '' + children
;
993 return node
.lastChild
;
999 content: function(node
, children
) {
1000 if (!this.elem(node
))
1003 while (node
.firstChild
)
1004 node
.removeChild(node
.firstChild
);
1006 return this.append(node
, children
);
1009 attr: function(node
, key
, val
) {
1010 if (!this.elem(node
))
1015 if (typeof(key
) === 'object' && key
!== null)
1017 else if (typeof(key
) === 'string')
1018 attr
= {}, attr
[key
] = val
;
1021 if (!attr
.hasOwnProperty(key
) || attr
[key
] === null || attr
[key
] === undefined)
1024 switch (typeof(attr
[key
])) {
1026 node
.addEventListener(key
, attr
[key
]);
1030 node
.setAttribute(key
, JSON
.stringify(attr
[key
]));
1034 node
.setAttribute(key
, attr
[key
]);
1039 create: function() {
1040 var html
= arguments
[0],
1041 attr
= (arguments
[1] instanceof Object
&& !Array
.isArray(arguments
[1])) ? arguments
[1] : null,
1042 data
= attr
? arguments
[2] : arguments
[1],
1045 if (this.elem(html
))
1047 else if (html
.charCodeAt(0) === 60)
1048 elem
= this.parse(html
);
1050 elem
= document
.createElement(html
);
1055 this.attr(elem
, attr
);
1056 this.append(elem
, data
);
1062 XHR
= Class
.extend({
1063 __name__
: 'LuCI.XHR',
1064 __init__: function() {
1065 if (window
.console
&& console
.debug
)
1066 console
.debug('Direct use XHR() is deprecated, please use L.Request instead');
1069 _response: function(cb
, res
, json
, duration
) {
1071 cb(res
, json
, duration
);
1075 get: function(url
, data
, callback
, timeout
) {
1077 L
.get(url
, data
, this._response
.bind(this, callback
), timeout
);
1080 post: function(url
, data
, callback
, timeout
) {
1082 L
.post(url
, data
, this._response
.bind(this, callback
), timeout
);
1085 cancel: function() { delete this.active
},
1086 busy: function() { return (this.active
=== true) },
1087 abort: function() {},
1088 send_form: function() { throw 'Not implemented' },
1091 XHR
.get = function() { return window
.L
.get.apply(window
.L
, arguments
) };
1092 XHR
.post = function() { return window
.L
.post
.apply(window
.L
, arguments
) };
1093 XHR
.poll = function() { return window
.L
.poll
.apply(window
.L
, arguments
) };
1094 XHR
.stop
= Request
.poll
.remove
.bind(Request
.poll
);
1095 XHR
.halt
= Request
.poll
.stop
.bind(Request
.poll
);
1096 XHR
.run
= Request
.poll
.start
.bind(Request
.poll
);
1097 XHR
.running
= Request
.poll
.active
.bind(Request
.poll
);
1101 })(window
, document
);