luci-base: luci.js: rework L.error()
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
1 (function(window, document, undefined) {
2 'use strict';
3
4 /* Object.assign polyfill for IE */
5 if (typeof Object.assign !== 'function') {
6 Object.defineProperty(Object, 'assign', {
7 value: function assign(target, varArgs) {
8 if (target == null)
9 throw new TypeError('Cannot convert undefined or null to object');
10
11 var to = Object(target);
12
13 for (var index = 1; index < arguments.length; index++)
14 if (arguments[index] != null)
15 for (var nextKey in arguments[index])
16 if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
17 to[nextKey] = arguments[index][nextKey];
18
19 return to;
20 },
21 writable: true,
22 configurable: true
23 });
24 }
25
26 /*
27 * Class declaration and inheritance helper
28 */
29
30 var toCamelCase = function(s) {
31 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
32 };
33
34 var superContext = null, Class = Object.assign(function() {}, {
35 extend: function(properties) {
36 var props = {
37 __base__: { value: this.prototype },
38 __name__: { value: properties.__name__ || 'anonymous' }
39 };
40
41 var ClassConstructor = function() {
42 if (!(this instanceof ClassConstructor))
43 throw new TypeError('Constructor must not be called without "new"');
44
45 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
46 if (typeof(this.__init__) != 'function')
47 throw new TypeError('Class __init__ member is not a function');
48
49 this.__init__.apply(this, arguments)
50 }
51 else {
52 this.super('__init__', arguments);
53 }
54 };
55
56 for (var key in properties)
57 if (!props[key] && properties.hasOwnProperty(key))
58 props[key] = { value: properties[key], writable: true };
59
60 ClassConstructor.prototype = Object.create(this.prototype, props);
61 ClassConstructor.prototype.constructor = ClassConstructor;
62 Object.assign(ClassConstructor, this);
63 ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
64
65 return ClassConstructor;
66 },
67
68 singleton: function(properties /*, ... */) {
69 return Class.extend(properties)
70 .instantiate(Class.prototype.varargs(arguments, 1));
71 },
72
73 instantiate: function(args) {
74 return new (Function.prototype.bind.apply(this,
75 Class.prototype.varargs(args, 0, null)))();
76 },
77
78 call: function(self, method) {
79 if (typeof(this.prototype[method]) != 'function')
80 throw new ReferenceError(method + ' is not defined in class');
81
82 return this.prototype[method].apply(self, self.varargs(arguments, 1));
83 },
84
85 isSubclass: function(_class) {
86 return (_class != null &&
87 typeof(_class) == 'function' &&
88 _class.prototype instanceof this);
89 },
90
91 prototype: {
92 varargs: function(args, offset /*, ... */) {
93 return Array.prototype.slice.call(arguments, 2)
94 .concat(Array.prototype.slice.call(args, offset));
95 },
96
97 super: function(key, callArgs) {
98 for (superContext = Object.getPrototypeOf(superContext ||
99 Object.getPrototypeOf(this));
100 superContext && !superContext.hasOwnProperty(key);
101 superContext = Object.getPrototypeOf(superContext)) { }
102
103 if (!superContext)
104 return null;
105
106 var res = superContext[key];
107
108 if (arguments.length > 1) {
109 if (typeof(res) != 'function')
110 throw new ReferenceError(key + ' is not a function in base class');
111
112 if (typeof(callArgs) != 'object')
113 callArgs = this.varargs(arguments, 1);
114
115 res = res.apply(this, callArgs);
116 }
117
118 superContext = null;
119
120 return res;
121 },
122
123 toString: function() {
124 var s = '[' + this.constructor.displayName + ']', f = true;
125 for (var k in this) {
126 if (this.hasOwnProperty(k)) {
127 s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
128 f = false;
129 }
130 }
131 return s + (f ? '' : '}');
132 }
133 }
134 });
135
136
137 /*
138 * HTTP Request helper
139 */
140
141 var Headers = Class.extend({
142 __name__: 'LuCI.XHR.Headers',
143 __init__: function(xhr) {
144 var hdrs = this.headers = {};
145 xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
146 var m = /^([^:]+):(.*)$/.exec(line);
147 if (m != null)
148 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
149 });
150 },
151
152 has: function(name) {
153 return this.headers.hasOwnProperty(String(name).toLowerCase());
154 },
155
156 get: function(name) {
157 var key = String(name).toLowerCase();
158 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
159 }
160 });
161
162 var Response = Class.extend({
163 __name__: 'LuCI.XHR.Response',
164 __init__: function(xhr, url, duration, headers, content) {
165 this.ok = (xhr.status >= 200 && xhr.status <= 299);
166 this.status = xhr.status;
167 this.statusText = xhr.statusText;
168 this.headers = (headers != null) ? headers : new Headers(xhr);
169 this.duration = duration;
170 this.url = url;
171 this.xhr = xhr;
172
173 if (content != null && typeof(content) == 'object') {
174 this.responseJSON = content;
175 this.responseText = null;
176 }
177 else if (content != null) {
178 this.responseJSON = null;
179 this.responseText = String(content);
180 }
181 else {
182 this.responseJSON = null;
183 this.responseText = xhr.responseText;
184 }
185 },
186
187 clone: function(content) {
188 var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
189
190 copy.ok = this.ok;
191 copy.status = this.status;
192 copy.statusText = this.statusText;
193
194 return copy;
195 },
196
197 json: function() {
198 if (this.responseJSON == null)
199 this.responseJSON = JSON.parse(this.responseText);
200
201 return this.responseJSON;
202 },
203
204 text: function() {
205 if (this.responseText == null && this.responseJSON != null)
206 this.responseText = JSON.stringify(this.responseJSON);
207
208 return this.responseText;
209 }
210 });
211
212
213 var requestQueue = [],
214 rpcBaseURL = null;
215
216 function isQueueableRequest(opt) {
217 if (!classes.rpc)
218 return false;
219
220 if (opt.method != 'POST' || typeof(opt.content) != 'object')
221 return false;
222
223 if (opt.nobatch === true)
224 return false;
225
226 if (rpcBaseURL == null)
227 rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
228
229 return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
230 }
231
232 function flushRequestQueue() {
233 if (!requestQueue.length)
234 return;
235
236 var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
237 batch = [];
238
239 for (var i = 0; i < requestQueue.length; i++) {
240 batch[i] = requestQueue[i];
241 reqopt.content[i] = batch[i][0].content;
242 }
243
244 requestQueue.length = 0;
245
246 Request.request(rpcBaseURL, reqopt).then(function(reply) {
247 var json = null, req = null;
248
249 try { json = reply.json() }
250 catch(e) { }
251
252 while ((req = batch.shift()) != null)
253 if (Array.isArray(json) && json.length)
254 req[2].call(reqopt, reply.clone(json.shift()));
255 else
256 req[1].call(reqopt, new Error('No related RPC reply'));
257 }).catch(function(error) {
258 var req = null;
259
260 while ((req = batch.shift()) != null)
261 req[1].call(reqopt, error);
262 });
263 }
264
265 var Request = Class.singleton({
266 __name__: 'LuCI.Request',
267
268 interceptors: [],
269
270 expandURL: function(url) {
271 if (!/^(?:[^/]+:)?\/\//.test(url))
272 url = location.protocol + '//' + location.host + url;
273
274 return url;
275 },
276
277 request: function(target, options) {
278 var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
279 opt = Object.assign({}, options, state),
280 content = null,
281 contenttype = null,
282 callback = this.handleReadyStateChange;
283
284 return new Promise(function(resolveFn, rejectFn) {
285 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
286 opt.method = String(opt.method || 'GET').toUpperCase();
287
288 if ('query' in opt) {
289 var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
290 if (opt.query[k] != null) {
291 var v = (typeof(opt.query[k]) == 'object')
292 ? JSON.stringify(opt.query[k])
293 : String(opt.query[k]);
294
295 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
296 }
297 else {
298 return encodeURIComponent(k);
299 }
300 }).join('&') : '';
301
302 if (q !== '') {
303 switch (opt.method) {
304 case 'GET':
305 case 'HEAD':
306 case 'OPTIONS':
307 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
308 break;
309
310 default:
311 if (content == null) {
312 content = q;
313 contenttype = 'application/x-www-form-urlencoded';
314 }
315 }
316 }
317 }
318
319 if (!opt.cache)
320 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
321
322 if (isQueueableRequest(opt)) {
323 requestQueue.push([opt, rejectFn, resolveFn]);
324 requestAnimationFrame(flushRequestQueue);
325 return;
326 }
327
328 if ('username' in opt && 'password' in opt)
329 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
330 else
331 opt.xhr.open(opt.method, opt.url, true);
332
333 opt.xhr.responseType = 'text';
334 opt.xhr.overrideMimeType('application/octet-stream');
335
336 if ('timeout' in opt)
337 opt.xhr.timeout = +opt.timeout;
338
339 if ('credentials' in opt)
340 opt.xhr.withCredentials = !!opt.credentials;
341
342 if (opt.content != null) {
343 switch (typeof(opt.content)) {
344 case 'function':
345 content = opt.content(xhr);
346 break;
347
348 case 'object':
349 content = JSON.stringify(opt.content);
350 contenttype = 'application/json';
351 break;
352
353 default:
354 content = String(opt.content);
355 }
356 }
357
358 if ('headers' in opt)
359 for (var header in opt.headers)
360 if (opt.headers.hasOwnProperty(header)) {
361 if (header.toLowerCase() != 'content-type')
362 opt.xhr.setRequestHeader(header, opt.headers[header]);
363 else
364 contenttype = opt.headers[header];
365 }
366
367 if (contenttype != null)
368 opt.xhr.setRequestHeader('Content-Type', contenttype);
369
370 try {
371 opt.xhr.send(content);
372 }
373 catch (e) {
374 rejectFn.call(opt, e);
375 }
376 });
377 },
378
379 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
380 var xhr = this.xhr;
381
382 if (xhr.readyState !== 4)
383 return;
384
385 if (xhr.status === 0 && xhr.statusText === '') {
386 rejectFn.call(this, new Error('XHR request aborted by browser'));
387 }
388 else {
389 var response = new Response(
390 xhr, xhr.responseURL || this.url, Date.now() - this.start);
391
392 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
393 .then(resolveFn.bind(this, response))
394 .catch(rejectFn.bind(this));
395 }
396
397 try {
398 xhr.abort();
399 }
400 catch(e) {}
401 },
402
403 get: function(url, options) {
404 return this.request(url, Object.assign({ method: 'GET' }, options));
405 },
406
407 post: function(url, data, options) {
408 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
409 },
410
411 addInterceptor: function(interceptorFn) {
412 if (typeof(interceptorFn) == 'function')
413 this.interceptors.push(interceptorFn);
414 return interceptorFn;
415 },
416
417 removeInterceptor: function(interceptorFn) {
418 var oldlen = this.interceptors.length, i = oldlen;
419 while (i--)
420 if (this.interceptors[i] === interceptorFn)
421 this.interceptors.splice(i, 1);
422 return (this.interceptors.length < oldlen);
423 },
424
425 poll: {
426 add: function(interval, url, options, callback) {
427 if (isNaN(interval) || interval <= 0)
428 throw new TypeError('Invalid poll interval');
429
430 var ival = interval >>> 0,
431 opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
432
433 return Poll.add(function() {
434 return Request.request(url, options).then(function(res) {
435 if (!Poll.active())
436 return;
437
438 try {
439 callback(res, res.json(), res.duration);
440 }
441 catch (err) {
442 callback(res, null, res.duration);
443 }
444 });
445 }, ival);
446 },
447
448 remove: function(entry) { return Poll.remove(entry) },
449 start: function() { return Poll.start() },
450 stop: function() { return Poll.stop() },
451 active: function() { return Poll.active() }
452 }
453 });
454
455 var Poll = Class.singleton({
456 __name__: 'LuCI.Poll',
457
458 queue: [],
459
460 add: function(fn, interval) {
461 if (interval == null || interval <= 0)
462 interval = window.L ? window.L.env.pollinterval : null;
463
464 if (isNaN(interval) || typeof(fn) != 'function')
465 throw new TypeError('Invalid argument to LuCI.Poll.add()');
466
467 for (var i = 0; i < this.queue.length; i++)
468 if (this.queue[i].fn === fn)
469 return false;
470
471 var e = {
472 r: true,
473 i: interval >>> 0,
474 fn: fn
475 };
476
477 this.queue.push(e);
478
479 if (this.tick != null && !this.active())
480 this.start();
481
482 return true;
483 },
484
485 remove: function(entry) {
486 if (typeof(fn) != 'function')
487 throw new TypeError('Invalid argument to LuCI.Poll.remove()');
488
489 var len = this.queue.length;
490
491 for (var i = len; i > 0; i--)
492 if (this.queue[i-1].fn === fn)
493 this.queue.splice(i-1, 1);
494
495 if (!this.queue.length && this.stop())
496 this.tick = 0;
497
498 return (this.queue.length != len);
499 },
500
501 start: function() {
502 if (this.active())
503 return false;
504
505 this.tick = 0;
506
507 if (this.queue.length) {
508 this.timer = window.setInterval(this.step, 1000);
509 this.step();
510 document.dispatchEvent(new CustomEvent('poll-start'));
511 }
512
513 return true;
514 },
515
516 stop: function() {
517 if (!this.active())
518 return false;
519
520 document.dispatchEvent(new CustomEvent('poll-stop'));
521 window.clearInterval(this.timer);
522 delete this.timer;
523 delete this.tick;
524 return true;
525 },
526
527 step: function() {
528 for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
529 if ((Poll.tick % e.i) != 0)
530 continue;
531
532 if (!e.r)
533 continue;
534
535 e.r = false;
536
537 Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
538 }
539
540 Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
541 },
542
543 active: function() {
544 return (this.timer != null);
545 }
546 });
547
548
549 var dummyElem = null,
550 domParser = null,
551 originalCBIInit = null,
552 classes = {};
553
554 var LuCI = Class.extend({
555 __name__: 'LuCI',
556 __init__: function(env) {
557
558 document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
559 if (env.base_url == null || env.base_url == '')
560 env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
561 });
562
563 if (env.base_url == null)
564 this.error('InternalError', 'Cannot find url of luci.js');
565
566 Object.assign(this.env, env);
567
568 document.addEventListener('poll-start', function(ev) {
569 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
570 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
571 });
572 });
573
574 document.addEventListener('poll-stop', function(ev) {
575 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
576 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
577 });
578 });
579
580 var domReady = new Promise(function(resolveFn, rejectFn) {
581 document.addEventListener('DOMContentLoaded', resolveFn);
582 });
583
584 Promise.all([
585 domReady,
586 this.require('ui'),
587 this.require('form')
588 ]).then(this.setupDOM.bind(this)).catch(this.error);
589
590 originalCBIInit = window.cbi_init;
591 window.cbi_init = function() {};
592 },
593
594 raise: function(type, fmt /*, ...*/) {
595 var e = null,
596 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
597 stack = null;
598
599 if (type instanceof Error) {
600 e = type;
601 stack = (e.stack || '').split(/\n/);
602
603 if (msg)
604 e.message = msg + ': ' + e.message;
605 }
606 else {
607 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
608 e.name = type || 'Error';
609 }
610
611 if (window.console && console.debug)
612 console.debug(e);
613
614 throw e;
615 },
616
617 error: function(type, fmt /*, ...*/) {
618 try {
619 L.raise.apply(L, Array.prototype.slice.call(arguments));
620 }
621 catch (e) {
622 var stack = (e.stack || '').split(/\n/).map(function(frame) {
623 frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
624 return frame ? ' ' + frame : '';
625 });
626
627 if (!/^ at /.test(stack[0]))
628 stack.shift();
629
630 if (/\braise /.test(stack[0]))
631 stack.shift();
632
633 if (/\berror /.test(stack[0]))
634 stack.shift();
635
636 stack = stack.length ? '\n' + stack.join('\n') : '';
637
638 if (L.ui)
639 L.ui.showModal(e.name || _('Runtime error'),
640 E('pre', { 'class': 'alert-message error' }, e.message + stack));
641 else
642 L.dom.content(document.querySelector('#maincontent'),
643 E('pre', { 'class': 'alert-message error' }, e + stack));
644
645 throw e;
646 }
647 },
648
649 bind: function(fn, self /*, ... */) {
650 return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
651 },
652
653 /* Class require */
654 require: function(name, from) {
655 var L = this, url = null, from = from || [];
656
657 /* Class already loaded */
658 if (classes[name] != null) {
659 /* Circular dependency */
660 if (from.indexOf(name) != -1)
661 L.error('DependencyError',
662 'Circular dependency: class "%s" depends on "%s"',
663 name, from.join('" which depends on "'));
664
665 return classes[name];
666 }
667
668 url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
669 from = [ name ].concat(from);
670
671 var compileClass = function(res) {
672 if (!res.ok)
673 L.error('NetworkError',
674 'HTTP error %d while loading class file "%s"', res.status, url);
675
676 var source = res.text(),
677 requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
678 strictmatch = /^use[ \t]+strict$/,
679 depends = [],
680 args = '';
681
682 /* find require statements in source */
683 for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
684 var chr = source.charCodeAt(i);
685
686 if (esc) {
687 esc = false;
688 }
689 else if (chr == 92) {
690 esc = true;
691 }
692 else if (chr == quote) {
693 var s = source.substring(off, i),
694 m = requirematch.exec(s);
695
696 if (m) {
697 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
698 depends.push(L.require(dep, from));
699 args += ', ' + as;
700 }
701 else if (!strictmatch.exec(s)) {
702 break;
703 }
704
705 off = -1;
706 quote = -1;
707 }
708 else if (quote == -1 && (chr == 34 || chr == 39)) {
709 off = i + 1;
710 quote = chr;
711 }
712 }
713
714 /* load dependencies and instantiate class */
715 return Promise.all(depends).then(function(instances) {
716 var _factory, _class;
717
718 try {
719 _factory = eval(
720 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
721 .format(args, source, res.url));
722 }
723 catch (error) {
724 L.error('SyntaxError', '%s\n in %s:%s',
725 error.message, res.url, error.lineNumber || '?');
726 }
727
728 _factory.displayName = toCamelCase(name + 'ClassFactory');
729 _class = _factory.apply(_factory, [window, document, L].concat(instances));
730
731 if (!Class.isSubclass(_class))
732 L.error('TypeError', '"%s" factory yields invalid constructor', name);
733
734 if (_class.displayName == 'AnonymousClass')
735 _class.displayName = toCamelCase(name + 'Class');
736
737 var ptr = Object.getPrototypeOf(L),
738 parts = name.split(/\./),
739 instance = new _class();
740
741 for (var i = 0; ptr && i < parts.length - 1; i++)
742 ptr = ptr[parts[i]];
743
744 if (ptr)
745 ptr[parts[i]] = instance;
746
747 classes[name] = instance;
748
749 return instance;
750 });
751 };
752
753 /* Request class file */
754 classes[name] = Request.get(url, { cache: true })
755 .then(compileClass)
756 .catch(L.error);
757
758 return classes[name];
759 },
760
761 /* DOM setup */
762 setupDOM: function(ev) {
763 Request.addInterceptor(function(res) {
764 if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
765 return;
766
767 Poll.stop();
768
769 L.ui.showModal(_('Session expired'), [
770 E('div', { class: 'alert-message warning' },
771 _('A new login is required since the authentication session expired.')),
772 E('div', { class: 'right' },
773 E('div', {
774 class: 'btn primary',
775 click: function() {
776 var loc = window.location;
777 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
778 }
779 }, _('To login…')))
780 ]);
781
782 throw 'Session expired';
783 });
784
785 originalCBIInit();
786
787 Poll.start();
788
789 document.dispatchEvent(new CustomEvent('luci-loaded'));
790 },
791
792 env: {},
793
794 /* URL construction helpers */
795 path: function(prefix, parts) {
796 var url = [ prefix || '' ];
797
798 for (var i = 0; i < parts.length; i++)
799 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
800 url.push('/', parts[i]);
801
802 if (url.length === 1)
803 url.push('/');
804
805 return url.join('');
806 },
807
808 url: function() {
809 return this.path(this.env.scriptname, arguments);
810 },
811
812 resource: function() {
813 return this.path(this.env.resource, arguments);
814 },
815
816 location: function() {
817 return this.path(this.env.scriptname, this.env.requestpath);
818 },
819
820
821 /* HTTP resource fetching */
822 get: function(url, args, cb) {
823 return this.poll(null, url, args, cb, false);
824 },
825
826 post: function(url, args, cb) {
827 return this.poll(null, url, args, cb, true);
828 },
829
830 poll: function(interval, url, args, cb, post) {
831 if (interval !== null && interval <= 0)
832 interval = this.env.pollinterval;
833
834 var data = post ? { token: this.env.token } : null,
835 method = post ? 'POST' : 'GET';
836
837 if (!/^(?:\/|\S+:\/\/)/.test(url))
838 url = this.url(url);
839
840 if (args != null)
841 data = Object.assign(data || {}, args);
842
843 if (interval !== null)
844 return Request.poll.add(interval, url, { method: method, query: data }, cb);
845 else
846 return Request.request(url, { method: method, query: data })
847 .then(function(res) {
848 var json = null;
849 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
850 try { json = res.json() } catch(e) {}
851 cb(res.xhr, json, res.duration);
852 });
853 },
854
855 stop: function(entry) { return Poll.remove(entry) },
856 halt: function() { return Poll.stop() },
857 run: function() { return Poll.start() },
858
859 /* DOM manipulation */
860 dom: Class.singleton({
861 __name__: 'LuCI.DOM',
862
863 elem: function(e) {
864 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
865 },
866
867 parse: function(s) {
868 var elem;
869
870 try {
871 domParser = domParser || new DOMParser();
872 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
873 }
874 catch(e) {}
875
876 if (!elem) {
877 try {
878 dummyElem = dummyElem || document.createElement('div');
879 dummyElem.innerHTML = s;
880 elem = dummyElem.firstChild;
881 }
882 catch (e) {}
883 }
884
885 return elem || null;
886 },
887
888 matches: function(node, selector) {
889 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
890 return m ? m.call(node, selector) : false;
891 },
892
893 parent: function(node, selector) {
894 if (this.elem(node) && node.closest)
895 return node.closest(selector);
896
897 while (this.elem(node))
898 if (this.matches(node, selector))
899 return node;
900 else
901 node = node.parentNode;
902
903 return null;
904 },
905
906 append: function(node, children) {
907 if (!this.elem(node))
908 return null;
909
910 if (Array.isArray(children)) {
911 for (var i = 0; i < children.length; i++)
912 if (this.elem(children[i]))
913 node.appendChild(children[i]);
914 else if (children !== null && children !== undefined)
915 node.appendChild(document.createTextNode('' + children[i]));
916
917 return node.lastChild;
918 }
919 else if (typeof(children) === 'function') {
920 return this.append(node, children(node));
921 }
922 else if (this.elem(children)) {
923 return node.appendChild(children);
924 }
925 else if (children !== null && children !== undefined) {
926 node.innerHTML = '' + children;
927 return node.lastChild;
928 }
929
930 return null;
931 },
932
933 content: function(node, children) {
934 if (!this.elem(node))
935 return null;
936
937 var dataNodes = node.querySelectorAll('[data-idref]');
938
939 for (var i = 0; i < dataNodes.length; i++)
940 delete this.registry[dataNodes[i].getAttribute('data-idref')];
941
942 while (node.firstChild)
943 node.removeChild(node.firstChild);
944
945 return this.append(node, children);
946 },
947
948 attr: function(node, key, val) {
949 if (!this.elem(node))
950 return null;
951
952 var attr = null;
953
954 if (typeof(key) === 'object' && key !== null)
955 attr = key;
956 else if (typeof(key) === 'string')
957 attr = {}, attr[key] = val;
958
959 for (key in attr) {
960 if (!attr.hasOwnProperty(key) || attr[key] == null)
961 continue;
962
963 switch (typeof(attr[key])) {
964 case 'function':
965 node.addEventListener(key, attr[key]);
966 break;
967
968 case 'object':
969 node.setAttribute(key, JSON.stringify(attr[key]));
970 break;
971
972 default:
973 node.setAttribute(key, attr[key]);
974 }
975 }
976 },
977
978 create: function() {
979 var html = arguments[0],
980 attr = arguments[1],
981 data = arguments[2],
982 elem;
983
984 if (!(attr instanceof Object) || Array.isArray(attr))
985 data = attr, attr = null;
986
987 if (Array.isArray(html)) {
988 elem = document.createDocumentFragment();
989 for (var i = 0; i < html.length; i++)
990 elem.appendChild(this.create(html[i]));
991 }
992 else if (this.elem(html)) {
993 elem = html;
994 }
995 else if (html.charCodeAt(0) === 60) {
996 elem = this.parse(html);
997 }
998 else {
999 elem = document.createElement(html);
1000 }
1001
1002 if (!elem)
1003 return null;
1004
1005 this.attr(elem, attr);
1006 this.append(elem, data);
1007
1008 return elem;
1009 },
1010
1011 registry: {},
1012
1013 data: function(node, key, val) {
1014 var id = node.getAttribute('data-idref');
1015
1016 /* clear all data */
1017 if (arguments.length > 1 && key == null) {
1018 if (id != null) {
1019 node.removeAttribute('data-idref');
1020 val = this.registry[id]
1021 delete this.registry[id];
1022 return val;
1023 }
1024
1025 return null;
1026 }
1027
1028 /* clear a key */
1029 else if (arguments.length > 2 && key != null && val == null) {
1030 if (id != null) {
1031 val = this.registry[id][key];
1032 delete this.registry[id][key];
1033 return val;
1034 }
1035
1036 return null;
1037 }
1038
1039 /* set a key */
1040 else if (arguments.length > 2 && key != null && val != null) {
1041 if (id == null) {
1042 do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
1043 while (this.registry.hasOwnProperty(id));
1044
1045 node.setAttribute('data-idref', id);
1046 this.registry[id] = {};
1047 }
1048
1049 return (this.registry[id][key] = val);
1050 }
1051
1052 /* get all data */
1053 else if (arguments.length == 1) {
1054 if (id != null)
1055 return this.registry[id];
1056
1057 return null;
1058 }
1059
1060 /* get a key */
1061 else if (arguments.length == 2) {
1062 if (id != null)
1063 return this.registry[id][key];
1064 }
1065
1066 return null;
1067 },
1068
1069 bindClassInstance: function(node, inst) {
1070 if (!(inst instanceof Class))
1071 L.error('TypeError', 'Argument must be a class instance');
1072
1073 return this.data(node, '_class', inst);
1074 },
1075
1076 findClassInstance: function(node) {
1077 var inst = null;
1078
1079 do {
1080 inst = this.data(node, '_class');
1081 node = node.parentNode;
1082 }
1083 while (!(inst instanceof Class) && node != null);
1084
1085 return inst;
1086 },
1087
1088 callClassMethod: function(node, method /*, ... */) {
1089 var inst = this.findClassInstance(node);
1090
1091 if (inst == null || typeof(inst[method]) != 'function')
1092 return null;
1093
1094 return inst[method].apply(inst, inst.varargs(arguments, 2));
1095 }
1096 }),
1097
1098 Poll: Poll,
1099 Class: Class,
1100 Request: Request,
1101
1102 view: Class.extend({
1103 __name__: 'LuCI.View',
1104
1105 __init__: function() {
1106 var vp = document.getElementById('view');
1107
1108 L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
1109
1110 return Promise.resolve(this.load())
1111 .then(L.bind(this.render, this))
1112 .then(L.bind(function(nodes) {
1113 var vp = document.getElementById('view');
1114
1115 L.dom.content(vp, nodes);
1116 L.dom.append(vp, this.addFooter());
1117 }, this)).catch(L.error);
1118 },
1119
1120 load: function() {},
1121 render: function() {},
1122
1123 handleSave: function(ev) {
1124 var tasks = [];
1125
1126 document.getElementById('maincontent')
1127 .querySelectorAll('.cbi-map').forEach(function(map) {
1128 tasks.push(L.dom.callClassMethod(map, 'save'));
1129 });
1130
1131 return Promise.all(tasks);
1132 },
1133
1134 handleSaveApply: function(ev) {
1135 return this.handleSave(ev).then(function() {
1136 L.ui.changes.apply(true);
1137 });
1138 },
1139
1140 handleReset: function(ev) {
1141 var tasks = [];
1142
1143 document.getElementById('maincontent')
1144 .querySelectorAll('.cbi-map').forEach(function(map) {
1145 tasks.push(L.dom.callClassMethod(map, 'reset'));
1146 });
1147
1148 return Promise.all(tasks);
1149 },
1150
1151 addFooter: function() {
1152 var footer = E([]),
1153 mc = document.getElementById('maincontent');
1154
1155 if (mc.querySelector('.cbi-map')) {
1156 footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
1157 E('input', {
1158 'class': 'cbi-button cbi-button-apply',
1159 'type': 'button',
1160 'value': _('Save & Apply'),
1161 'click': L.bind(this.handleSaveApply, this)
1162 }), ' ',
1163 E('input', {
1164 'class': 'cbi-button cbi-button-save',
1165 'type': 'submit',
1166 'value': _('Save'),
1167 'click': L.bind(this.handleSave, this)
1168 }), ' ',
1169 E('input', {
1170 'class': 'cbi-button cbi-button-reset',
1171 'type': 'button',
1172 'value': _('Reset'),
1173 'click': L.bind(this.handleReset, this)
1174 })
1175 ]));
1176 }
1177
1178 return footer;
1179 }
1180 })
1181 });
1182
1183 var XHR = Class.extend({
1184 __name__: 'LuCI.XHR',
1185 __init__: function() {
1186 if (window.console && console.debug)
1187 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
1188 },
1189
1190 _response: function(cb, res, json, duration) {
1191 if (this.active)
1192 cb(res, json, duration);
1193 delete this.active;
1194 },
1195
1196 get: function(url, data, callback, timeout) {
1197 this.active = true;
1198 L.get(url, data, this._response.bind(this, callback), timeout);
1199 },
1200
1201 post: function(url, data, callback, timeout) {
1202 this.active = true;
1203 L.post(url, data, this._response.bind(this, callback), timeout);
1204 },
1205
1206 cancel: function() { delete this.active },
1207 busy: function() { return (this.active === true) },
1208 abort: function() {},
1209 send_form: function() { L.error('InternalError', 'Not implemented') },
1210 });
1211
1212 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
1213 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
1214 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
1215 XHR.stop = Request.poll.remove.bind(Request.poll);
1216 XHR.halt = Request.poll.stop.bind(Request.poll);
1217 XHR.run = Request.poll.start.bind(Request.poll);
1218 XHR.running = Request.poll.active.bind(Request.poll);
1219
1220 window.XHR = XHR;
1221 window.LuCI = LuCI;
1222 })(window, document);