luci-base: luci.js: add dynamic class loader
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
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) {
6 if (target == null)
7 throw new TypeError('Cannot convert undefined or null to object');
8
9 var to = Object(target);
10
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];
16
17 return to;
18 },
19 writable: true,
20 configurable: true
21 });
22 }
23
24 /*
25 * Class declaration and inheritance helper
26 */
27
28 var toCamelCase = function(s) {
29 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
30 };
31
32 var superContext = null, Class = Object.assign(function() {}, {
33 extend: function(properties) {
34 var props = {
35 __base__: { value: this.prototype },
36 __name__: { value: properties.__name__ || 'anonymous' }
37 };
38
39 var ClassConstructor = function() {
40 if (!(this instanceof ClassConstructor))
41 throw new TypeError('Constructor must not be called without "new"');
42
43 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
44 if (typeof(this.__init__) != 'function')
45 throw new TypeError('Class __init__ member is not a function');
46
47 this.__init__.apply(this, arguments)
48 }
49 else {
50 this.super('__init__', arguments);
51 }
52 };
53
54 for (var key in properties)
55 if (!props[key] && properties.hasOwnProperty(key))
56 props[key] = { value: properties[key], writable: true };
57
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');
62
63 return ClassConstructor;
64 },
65
66 singleton: function(properties /*, ... */) {
67 return Class.extend(properties)
68 .instantiate(Class.prototype.varargs(arguments, 1));
69 },
70
71 instantiate: function(args) {
72 return new (Function.prototype.bind.apply(this,
73 Class.prototype.varargs(args, 0, null)))();
74 },
75
76 call: function(self, method) {
77 if (typeof(this.prototype[method]) != 'function')
78 throw new ReferenceError(method + ' is not defined in class');
79
80 return this.prototype[method].apply(self, self.varargs(arguments, 1));
81 },
82
83 isSubclass: function(_class) {
84 return (_class != null &&
85 typeof(_class) == 'function' &&
86 _class.prototype instanceof this);
87 },
88
89 prototype: {
90 varargs: function(args, offset /*, ... */) {
91 return Array.prototype.slice.call(arguments, 2)
92 .concat(Array.prototype.slice.call(args, offset));
93 },
94
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)) { }
100
101 if (!superContext)
102 return null;
103
104 var res = superContext[key];
105
106 if (arguments.length > 1) {
107 if (typeof(res) != 'function')
108 throw new ReferenceError(key + ' is not a function in base class');
109
110 if (typeof(callArgs) != 'object')
111 callArgs = this.varargs(arguments, 1);
112
113 res = res.apply(this, callArgs);
114 }
115
116 superContext = null;
117
118 return res;
119 },
120
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';
126 f = false;
127 }
128 }
129 return s + (f ? '' : '}');
130 }
131 }
132 });
133
134
135 /*
136 * HTTP Request helper
137 */
138
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);
145 if (m != null)
146 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
147 });
148 },
149
150 has: function(name) {
151 return this.headers.hasOwnProperty(String(name).toLowerCase());
152 },
153
154 get: function(name) {
155 var key = String(name).toLowerCase();
156 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
157 }
158 });
159
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;
169 this.url = url;
170 this.xhr = xhr;
171 },
172
173 json: function() {
174 return JSON.parse(this.responseText);
175 },
176
177 text: function() {
178 return this.responseText;
179 }
180 });
181
182 Request = Class.singleton({
183 __name__: 'LuCI.Request',
184
185 interceptors: [],
186
187 request: function(target, options) {
188 var state = { xhr: new XMLHttpRequest(), url: target, start: Date.now() },
189 opt = Object.assign({}, options, state),
190 content = null,
191 contenttype = null,
192 callback = this.handleReadyStateChange;
193
194 return new Promise(function(resolveFn, rejectFn) {
195 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
196 opt.method = String(opt.method || 'GET').toUpperCase();
197
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]);
204
205 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
206 }
207 else {
208 return encodeURIComponent(k);
209 }
210 }).join('&') : '';
211
212 if (q !== '') {
213 switch (opt.method) {
214 case 'GET':
215 case 'HEAD':
216 case 'OPTIONS':
217 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
218 break;
219
220 default:
221 if (content == null) {
222 content = q;
223 contenttype = 'application/x-www-form-urlencoded';
224 }
225 }
226 }
227 }
228
229 if (!opt.cache)
230 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
231
232 if (!/^(?:[^/]+:)?\/\//.test(opt.url))
233 opt.url = location.protocol + '//' + location.host + opt.url;
234
235 if ('username' in opt && 'password' in opt)
236 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
237 else
238 opt.xhr.open(opt.method, opt.url, true);
239
240 opt.xhr.responseType = 'text';
241 opt.xhr.overrideMimeType('application/octet-stream');
242
243 if ('timeout' in opt)
244 opt.xhr.timeout = +opt.timeout;
245
246 if ('credentials' in opt)
247 opt.xhr.withCredentials = !!opt.credentials;
248
249 if (opt.content != null) {
250 switch (typeof(opt.content)) {
251 case 'function':
252 content = opt.content(xhr);
253 break;
254
255 case 'object':
256 content = JSON.stringify(opt.content);
257 contenttype = 'application/json';
258 break;
259
260 default:
261 content = String(opt.content);
262 }
263 }
264
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]);
270 else
271 contenttype = opt.headers[header];
272 }
273
274 if (contenttype != null)
275 opt.xhr.setRequestHeader('Content-Type', contenttype);
276
277 try {
278 opt.xhr.send(content);
279 }
280 catch (e) {
281 rejectFn.call(opt, e);
282 }
283 });
284 },
285
286 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
287 var xhr = this.xhr;
288
289 if (xhr.readyState !== 4)
290 return;
291
292 if (xhr.status === 0 && xhr.statusText === '') {
293 rejectFn.call(this, new Error('XHR request aborted by browser'));
294 }
295 else {
296 var response = new Response(
297 xhr, xhr.responseURL || this.url, Date.now() - this.start);
298
299 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
300 .then(resolveFn.bind(this, response))
301 .catch(rejectFn.bind(this));
302 }
303
304 try {
305 xhr.abort();
306 }
307 catch(e) {}
308 },
309
310 get: function(url, options) {
311 return this.request(url, Object.assign({ method: 'GET' }, options));
312 },
313
314 post: function(url, data, options) {
315 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
316 },
317
318 addInterceptor: function(interceptorFn) {
319 if (typeof(interceptorFn) == 'function')
320 this.interceptors.push(interceptorFn);
321 return interceptorFn;
322 },
323
324 removeInterceptor: function(interceptorFn) {
325 var oldlen = this.interceptors.length, i = oldlen;
326 while (i--)
327 if (this.interceptors[i] === interceptorFn)
328 this.interceptors.splice(i, 1);
329 return (this.interceptors.length < oldlen);
330 },
331
332 poll: Class.singleton({
333 __name__: 'LuCI.Request.Poll',
334
335 queue: [],
336
337 add: function(interval, url, options, callback) {
338 if (isNaN(interval) || interval <= 0)
339 throw new TypeError('Invalid poll interval');
340
341 var e = {
342 interval: interval,
343 url: url,
344 options: options,
345 callback: callback
346 };
347
348 this.queue.push(e);
349 return e;
350 },
351
352 remove: function(entry) {
353 var oldlen = this.queue.length, i = oldlen;
354
355 while (i--)
356 if (this.queue[i] === entry) {
357 delete this.queue[i].running;
358 this.queue.splice(i, 1);
359 }
360
361 if (!this.queue.length)
362 this.stop();
363
364 return (this.queue.length < oldlen);
365 },
366
367 start: function() {
368 if (!this.queue.length || this.active())
369 return false;
370
371 this.tick = 0;
372 this.timer = window.setInterval(this.step, 1000);
373 this.step();
374 document.dispatchEvent(new CustomEvent('poll-start'));
375 return true;
376 },
377
378 stop: function() {
379 if (!this.active())
380 return false;
381
382 document.dispatchEvent(new CustomEvent('poll-stop'));
383 window.clearInterval(this.timer);
384 delete this.timer;
385 delete this.tick;
386 return true;
387 },
388
389 step: function() {
390 Request.poll.queue.forEach(function(e) {
391 if ((Request.poll.tick % e.interval) != 0)
392 return;
393
394 if (e.running)
395 return;
396
397 var opts = Object.assign({}, e.options,
398 { timeout: e.interval * 1000 - 5 });
399
400 e.running = true;
401 Request.request(e.url, opts)
402 .then(function(res) {
403 if (!e.running || !Request.poll.active())
404 return;
405
406 try {
407 e.callback(res, res.json(), res.duration);
408 }
409 catch (err) {
410 e.callback(res, null, res.duration);
411 }
412 })
413 .finally(function() { delete e.running });
414 });
415
416 Request.poll.tick = (Request.poll.tick + 1) % Math.pow(2, 32);
417 },
418
419 active: function() {
420 return (this.timer != null);
421 }
422 })
423 });
424
425
426 var modalDiv = null,
427 tooltipDiv = null,
428 tooltipTimeout = null,
429 dummyElem = null,
430 domParser = null,
431 originalCBIInit = null,
432 classes = {};
433
434 LuCI = Class.extend({
435 __name__: 'LuCI',
436 __init__: function(env) {
437 Object.assign(this.env, env);
438
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 })));
442
443 tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
444
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);
449
450 document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
451
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' : '';
455 });
456 });
457
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' : '';
461 });
462 });
463
464 originalCBIInit = window.cbi_init;
465 window.cbi_init = function() {};
466 },
467
468 /* Class require */
469 require: function(name, from) {
470 var L = this, url = null, from = from || [];
471
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 "')));
478
479 return classes[name];
480 }
481
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, '/'));
486 });
487
488 if (url == null)
489 throw new Error('Cannot find url of luci.js');
490
491 from = [ name ].concat(from);
492
493 var compileClass = function(res) {
494 if (!res.ok)
495 throw new Error('HTTP error %d while loading class file "%s"'
496 .format(res.status, url));
497
498 var source = res.text(),
499 reqmatch = /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
500 depends = [],
501 args = '';
502
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));
507 args += ', ' + as;
508 }
509
510 /* load dependencies and instantiate class */
511 return Promise.all(depends).then(function(instances) {
512 try {
513 _factory = eval(
514 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
515 .format(args, source, res.url));
516 }
517 catch (error) {
518 throw new SyntaxError('%s\n in %s:%s'
519 .format(error.message, res.url, error.lineNumber || '?'));
520 }
521
522 _factory.displayName = toCamelCase(name + 'ClassFactory');
523 _class = _factory.apply(_factory, [window, document, L].concat(instances));
524
525 if (!Class.isSubclass(_class))
526 throw new TypeError('"%s" factory yields invalid constructor'
527 .format(name));
528
529 if (_class.displayName == 'AnonymousClass')
530 _class.displayName = toCamelCase(name + 'Class');
531
532 var ptr = Object.getPrototypeOf(L),
533 parts = name.split(/\./),
534 instance = new _class();
535
536 for (var i = 0; ptr && i < parts.length - 1; i++)
537 ptr = ptr[parts[i]];
538
539 if (!ptr)
540 throw new Error('Parent "%s" for class "%s" is missing'
541 .format(parts.slice(0, i).join('.'), name));
542
543 classes[name] = ptr[parts[i]] = instance;
544
545 return instance;
546 });
547 };
548
549 /* Request class file */
550 classes[name] = Request.get(url, { cache: true }).then(compileClass);
551
552 return classes[name];
553 },
554
555 /* DOM setup */
556 setupDOM: function(ev) {
557 this.tabs.init();
558
559 Request.addInterceptor(function(res) {
560 if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
561 return;
562
563 Request.poll.stop();
564
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' },
569 E('div', {
570 class: 'btn primary',
571 click: function() {
572 var loc = window.location;
573 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
574 }
575 }, _('To login…')))
576 ]);
577
578 return Promise.reject(new Error('Session expired'));
579 });
580
581 originalCBIInit();
582 Request.poll.start();
583 },
584
585 env: {},
586
587 /* URL construction helpers */
588 path: function(prefix, parts) {
589 var url = [ prefix || '' ];
590
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]);
594
595 if (url.length === 1)
596 url.push('/');
597
598 return url.join('');
599 },
600
601 url: function() {
602 return this.path(this.env.scriptname, arguments);
603 },
604
605 resource: function() {
606 return this.path(this.env.resource, arguments);
607 },
608
609 location: function() {
610 return this.path(this.env.scriptname, this.env.requestpath);
611 },
612
613
614 /* HTTP resource fetching */
615 get: function(url, args, cb) {
616 return this.poll(null, url, args, cb, false);
617 },
618
619 post: function(url, args, cb) {
620 return this.poll(null, url, args, cb, true);
621 },
622
623 poll: function(interval, url, args, cb, post) {
624 if (interval !== null && interval <= 0)
625 interval = this.env.pollinterval;
626
627 var data = post ? { token: this.env.token } : null,
628 method = post ? 'POST' : 'GET';
629
630 if (!/^(?:\/|\S+:\/\/)/.test(url))
631 url = this.url(url);
632
633 if (args != null)
634 data = Object.assign(data || {}, args);
635
636 if (interval !== null)
637 return Request.poll.add(interval, url, { method: method, query: data }, cb);
638 else
639 return Request.request(url, { method: method, query: data })
640 .then(function(res) {
641 var json = null;
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);
645 });
646 },
647
648 stop: function(entry) { return Request.poll.remove(entry) },
649 halt: function() { return Request.poll.stop() },
650 run: function() { return Request.poll.start() },
651
652
653 /* Modal dialog */
654 showModal: function(title, children) {
655 var dlg = modalDiv.firstElementChild;
656
657 dlg.setAttribute('class', 'modal');
658
659 this.dom.content(dlg, this.dom.create('h4', {}, title));
660 this.dom.append(dlg, children);
661
662 document.body.classList.add('modal-overlay-active');
663
664 return dlg;
665 },
666
667 hideModal: function() {
668 document.body.classList.remove('modal-overlay-active');
669 },
670
671
672 /* Tooltip */
673 showTooltip: function(ev) {
674 var target = findParent(ev.target, '[data-tooltip]');
675
676 if (!target)
677 return;
678
679 if (tooltipTimeout !== null) {
680 window.clearTimeout(tooltipTimeout);
681 tooltipTimeout = null;
682 }
683
684 var rect = target.getBoundingClientRect(),
685 x = rect.left + window.pageXOffset,
686 y = rect.top + rect.height + window.pageYOffset;
687
688 tooltipDiv.className = 'cbi-tooltip';
689 tooltipDiv.innerHTML = 'â–² ';
690 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
691
692 if (target.hasAttribute('data-tooltip-style'))
693 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
694
695 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
696 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
697 tooltipDiv.firstChild.data = 'â–¼ ' + tooltipDiv.firstChild.data.substr(2);
698 }
699
700 tooltipDiv.style.top = y + 'px';
701 tooltipDiv.style.left = x + 'px';
702 tooltipDiv.style.opacity = 1;
703
704 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
705 bubbles: true,
706 detail: { target: target }
707 }));
708 },
709
710 hideTooltip: function(ev) {
711 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
712 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
713 return;
714
715 if (tooltipTimeout !== null) {
716 window.clearTimeout(tooltipTimeout);
717 tooltipTimeout = null;
718 }
719
720 tooltipDiv.style.opacity = 0;
721 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
722
723 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
724 },
725
726
727 /* Widget helper */
728 itemlist: function(node, items, separators) {
729 var children = [];
730
731 if (!Array.isArray(separators))
732 separators = [ separators || E('br') ];
733
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],
737 cld = [];
738
739 children.push(E('span', { class: 'nowrap' }, [
740 items[i] ? E('strong', items[i] + ': ') : '',
741 items[i+1]
742 ]));
743
744 if ((i+2) < items.length)
745 children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
746 }
747 }
748
749 this.dom.content(node, children);
750
751 return node;
752 },
753
754 Class: Class,
755 Request: Request
756 });
757
758 /* Tabs */
759 LuCI.prototype.tabs = {
760 init: function() {
761 var groups = [], prevGroup = null, currGroup = null;
762
763 document.querySelectorAll('[data-tab]').forEach(function(tab) {
764 var parent = tab.parentNode;
765
766 if (!parent.hasAttribute('data-tab-group'))
767 parent.setAttribute('data-tab-group', groups.length);
768
769 currGroup = +parent.getAttribute('data-tab-group');
770
771 if (currGroup !== prevGroup) {
772 prevGroup = currGroup;
773
774 if (!groups[currGroup])
775 groups[currGroup] = [];
776 }
777
778 groups[currGroup].push(tab);
779 });
780
781 for (var i = 0; i < groups.length; i++)
782 this.initTabGroup(groups[i]);
783
784 document.addEventListener('dependency-update', this.updateTabs.bind(this));
785
786 this.updateTabs();
787
788 if (!groups.length)
789 this.setActiveTabId(-1, -1);
790 },
791
792 initTabGroup: function(panes) {
793 if (!Array.isArray(panes) || panes.length === 0)
794 return;
795
796 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
797 group = panes[0].parentNode,
798 groupId = +group.getAttribute('data-tab-group'),
799 selected = null;
800
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';
805
806 menu.appendChild(E('li', {
807 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
808 'data-tab': name
809 }, E('a', {
810 'href': '#',
811 'click': this.switchTab.bind(this)
812 }, title)));
813
814 if (active)
815 selected = i;
816 }
817
818 group.parentNode.insertBefore(menu, group);
819
820 if (selected === null) {
821 selected = this.getActiveTabId(groupId);
822
823 if (selected < 0 || selected >= panes.length)
824 selected = 0;
825
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');
829
830 this.setActiveTabId(groupId, selected);
831 }
832 },
833
834 getActiveTabState: function() {
835 var page = document.body.getAttribute('data-page');
836
837 try {
838 var val = JSON.parse(window.sessionStorage.getItem('tab'));
839 if (val.page === page && Array.isArray(val.groups))
840 return val;
841 }
842 catch(e) {}
843
844 window.sessionStorage.removeItem('tab');
845 return { page: page, groups: [] };
846 },
847
848 getActiveTabId: function(groupId) {
849 return +this.getActiveTabState().groups[groupId] || 0;
850 },
851
852 setActiveTabId: function(groupId, tabIndex) {
853 try {
854 var state = this.getActiveTabState();
855 state.groups[groupId] = tabIndex;
856
857 window.sessionStorage.setItem('tab', JSON.stringify(state));
858 }
859 catch (e) { return false; }
860
861 return true;
862 },
863
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;
869
870 if (!pane.firstElementChild) {
871 tab.style.display = 'none';
872 tab.classList.remove('flash');
873 }
874 else if (tab.style.display === 'none') {
875 tab.style.display = '';
876 requestAnimationFrame(function() { tab.classList.add('flash') });
877 }
878
879 if (n_errors) {
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');
883 }
884 else {
885 tab.removeAttribute('data-errors');
886 tab.removeAttribute('data-tooltip');
887 }
888 });
889 },
890
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'),
897 index = 0;
898
899 ev.preventDefault();
900
901 if (!tab.classList.contains('cbi-tab-disabled'))
902 return;
903
904 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
905 tab.classList.remove('cbi-tab');
906 tab.classList.remove('cbi-tab-disabled');
907 tab.classList.add(
908 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
909 });
910
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);
916 }
917 else {
918 pane.setAttribute('data-tab-active', 'false');
919 }
920
921 index++;
922 }
923 });
924 }
925 };
926
927 /* DOM manipulation */
928 LuCI.prototype.dom = {
929 elem: function(e) {
930 return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
931 },
932
933 parse: function(s) {
934 var elem;
935
936 try {
937 domParser = domParser || new DOMParser();
938 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
939 }
940 catch(e) {}
941
942 if (!elem) {
943 try {
944 dummyElem = dummyElem || document.createElement('div');
945 dummyElem.innerHTML = s;
946 elem = dummyElem.firstChild;
947 }
948 catch (e) {}
949 }
950
951 return elem || null;
952 },
953
954 matches: function(node, selector) {
955 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
956 return m ? m.call(node, selector) : false;
957 },
958
959 parent: function(node, selector) {
960 if (this.elem(node) && node.closest)
961 return node.closest(selector);
962
963 while (this.elem(node))
964 if (this.matches(node, selector))
965 return node;
966 else
967 node = node.parentNode;
968
969 return null;
970 },
971
972 append: function(node, children) {
973 if (!this.elem(node))
974 return null;
975
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]));
982
983 return node.lastChild;
984 }
985 else if (typeof(children) === 'function') {
986 return this.append(node, children(node));
987 }
988 else if (this.elem(children)) {
989 return node.appendChild(children);
990 }
991 else if (children !== null && children !== undefined) {
992 node.innerHTML = '' + children;
993 return node.lastChild;
994 }
995
996 return null;
997 },
998
999 content: function(node, children) {
1000 if (!this.elem(node))
1001 return null;
1002
1003 while (node.firstChild)
1004 node.removeChild(node.firstChild);
1005
1006 return this.append(node, children);
1007 },
1008
1009 attr: function(node, key, val) {
1010 if (!this.elem(node))
1011 return null;
1012
1013 var attr = null;
1014
1015 if (typeof(key) === 'object' && key !== null)
1016 attr = key;
1017 else if (typeof(key) === 'string')
1018 attr = {}, attr[key] = val;
1019
1020 for (key in attr) {
1021 if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
1022 continue;
1023
1024 switch (typeof(attr[key])) {
1025 case 'function':
1026 node.addEventListener(key, attr[key]);
1027 break;
1028
1029 case 'object':
1030 node.setAttribute(key, JSON.stringify(attr[key]));
1031 break;
1032
1033 default:
1034 node.setAttribute(key, attr[key]);
1035 }
1036 }
1037 },
1038
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],
1043 elem;
1044
1045 if (this.elem(html))
1046 elem = html;
1047 else if (html.charCodeAt(0) === 60)
1048 elem = this.parse(html);
1049 else
1050 elem = document.createElement(html);
1051
1052 if (!elem)
1053 return null;
1054
1055 this.attr(elem, attr);
1056 this.append(elem, data);
1057
1058 return elem;
1059 }
1060 };
1061
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');
1067 },
1068
1069 _response: function(cb, res, json, duration) {
1070 if (this.active)
1071 cb(res, json, duration);
1072 delete this.active;
1073 },
1074
1075 get: function(url, data, callback, timeout) {
1076 this.active = true;
1077 L.get(url, data, this._response.bind(this, callback), timeout);
1078 },
1079
1080 post: function(url, data, callback, timeout) {
1081 this.active = true;
1082 L.post(url, data, this._response.bind(this, callback), timeout);
1083 },
1084
1085 cancel: function() { delete this.active },
1086 busy: function() { return (this.active === true) },
1087 abort: function() {},
1088 send_form: function() { throw 'Not implemented' },
1089 });
1090
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);
1098
1099 window.XHR = XHR;
1100 window.LuCI = LuCI;
1101 })(window, document);