luci-base: luci.js: handle postprocessed sources
[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 dummyElem = null,
427 domParser = null,
428 originalCBIInit = null,
429 classes = {};
430
431 LuCI = Class.extend({
432 __name__: 'LuCI',
433 __init__: function(env) {
434
435 document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
436 if (env.base_url == null || env.base_url == '')
437 env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
438 });
439
440 if (env.base_url == null)
441 this.error('InternalError', 'Cannot find url of luci.js');
442
443 Object.assign(this.env, env);
444
445 document.addEventListener('poll-start', function(ev) {
446 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
447 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
448 });
449 });
450
451 document.addEventListener('poll-stop', function(ev) {
452 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
453 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
454 });
455 });
456
457 var domReady = new Promise(function(resolveFn, rejectFn) {
458 document.addEventListener('DOMContentLoaded', resolveFn);
459 });
460
461 Promise.all([
462 domReady,
463 this.require('ui'),
464 this.require('form')
465 ]).then(this.setupDOM.bind(this)).catch(this.error);
466
467 originalCBIInit = window.cbi_init;
468 window.cbi_init = function() {};
469 },
470
471 error: function(type, fmt /*, ...*/) {
472 var e = null,
473 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
474 stack = null;
475
476 if (type instanceof Error) {
477 e = type;
478 stack = (e.stack || '').split(/\n/);
479
480 if (msg)
481 e.message = msg + ': ' + e.message;
482 }
483 else {
484 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
485 e.name = type || 'Error';
486
487 try { throw new Error('stacktrace') }
488 catch (e2) { stack = (e2.stack || '').split(/\n/) }
489
490 /* IE puts the exception message into the first line */
491 if (stack[0] == 'Error: stacktrace')
492 stack.shift();
493
494 /* Pop L.error() invocation from stack */
495 stack.shift();
496 }
497
498 /* Append shortened & beautified stacktrace to message */
499 var trace = stack.join('\n')
500 .replace(/(.*?)@(.+):(\d+):(\d+)/g, ' at $1 ($2:$3:$4)');
501
502 if (e.message.indexOf(trace) == -1)
503 e.message += '\n' + trace;
504
505 if (window.console && console.debug)
506 console.debug(e);
507
508 if (this.ui)
509 this.ui.showModal(_('Runtime error'),
510 E('pre', { 'class': 'alert-message error' }, e));
511 else
512 L.dom.content(document.querySelector('#maincontent'),
513 E('pre', { 'class': 'alert-message error' }, e));
514
515 throw e;
516 },
517
518 bind: function(fn, self /*, ... */) {
519 return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
520 },
521
522 /* Class require */
523 require: function(name, from) {
524 var L = this, url = null, from = from || [];
525
526 /* Class already loaded */
527 if (classes[name] != null) {
528 /* Circular dependency */
529 if (from.indexOf(name) != -1)
530 L.error('DependencyError',
531 'Circular dependency: class "%s" depends on "%s"',
532 name, from.join('" which depends on "'));
533
534 return classes[name];
535 }
536
537 url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
538 from = [ name ].concat(from);
539
540 var compileClass = function(res) {
541 if (!res.ok)
542 L.error('NetworkError',
543 'HTTP error %d while loading class file "%s"', res.status, url);
544
545 var source = res.text(),
546 requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
547 strictmatch = /^use[ \t]+strict$/,
548 depends = [],
549 args = '';
550
551 /* find require statements in source */
552 for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
553 var chr = source.charCodeAt(i);
554
555 if (esc) {
556 esc = false;
557 }
558 else if (chr == 92) {
559 esc = true;
560 }
561 else if (chr == quote) {
562 var s = source.substring(off, i),
563 m = requirematch.exec(s);
564
565 if (m) {
566 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
567 depends.push(L.require(dep, from));
568 args += ', ' + as;
569 }
570 else if (!strictmatch.exec(s)) {
571 break;
572 }
573
574 off = -1;
575 quote = -1;
576 }
577 else if (quote == -1 && (chr == 34 || chr == 39)) {
578 off = i + 1;
579 quote = chr;
580 }
581 }
582
583 /* load dependencies and instantiate class */
584 return Promise.all(depends).then(function(instances) {
585 try {
586 _factory = eval(
587 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
588 .format(args, source, res.url));
589 }
590 catch (error) {
591 L.error('SyntaxError', '%s\n in %s:%s',
592 error.message, res.url, error.lineNumber || '?');
593 }
594
595 _factory.displayName = toCamelCase(name + 'ClassFactory');
596 _class = _factory.apply(_factory, [window, document, L].concat(instances));
597
598 if (!Class.isSubclass(_class))
599 L.error('TypeError', '"%s" factory yields invalid constructor', name);
600
601 if (_class.displayName == 'AnonymousClass')
602 _class.displayName = toCamelCase(name + 'Class');
603
604 var ptr = Object.getPrototypeOf(L),
605 parts = name.split(/\./),
606 instance = new _class();
607
608 for (var i = 0; ptr && i < parts.length - 1; i++)
609 ptr = ptr[parts[i]];
610
611 if (ptr)
612 ptr[parts[i]] = instance;
613
614 classes[name] = instance;
615
616 return instance;
617 });
618 };
619
620 /* Request class file */
621 classes[name] = Request.get(url, { cache: true })
622 .then(compileClass)
623 .catch(L.error);
624
625 return classes[name];
626 },
627
628 /* DOM setup */
629 setupDOM: function(ev) {
630 Request.addInterceptor(function(res) {
631 if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
632 return;
633
634 Request.poll.stop();
635
636 L.ui.showModal(_('Session expired'), [
637 E('div', { class: 'alert-message warning' },
638 _('A new login is required since the authentication session expired.')),
639 E('div', { class: 'right' },
640 E('div', {
641 class: 'btn primary',
642 click: function() {
643 var loc = window.location;
644 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
645 }
646 }, _('To login…')))
647 ]);
648
649 throw 'Session expired';
650 });
651
652 originalCBIInit();
653 Request.poll.start();
654
655 document.dispatchEvent(new CustomEvent('luci-loaded'));
656 },
657
658 env: {},
659
660 /* URL construction helpers */
661 path: function(prefix, parts) {
662 var url = [ prefix || '' ];
663
664 for (var i = 0; i < parts.length; i++)
665 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
666 url.push('/', parts[i]);
667
668 if (url.length === 1)
669 url.push('/');
670
671 return url.join('');
672 },
673
674 url: function() {
675 return this.path(this.env.scriptname, arguments);
676 },
677
678 resource: function() {
679 return this.path(this.env.resource, arguments);
680 },
681
682 location: function() {
683 return this.path(this.env.scriptname, this.env.requestpath);
684 },
685
686
687 /* HTTP resource fetching */
688 get: function(url, args, cb) {
689 return this.poll(null, url, args, cb, false);
690 },
691
692 post: function(url, args, cb) {
693 return this.poll(null, url, args, cb, true);
694 },
695
696 poll: function(interval, url, args, cb, post) {
697 if (interval !== null && interval <= 0)
698 interval = this.env.pollinterval;
699
700 var data = post ? { token: this.env.token } : null,
701 method = post ? 'POST' : 'GET';
702
703 if (!/^(?:\/|\S+:\/\/)/.test(url))
704 url = this.url(url);
705
706 if (args != null)
707 data = Object.assign(data || {}, args);
708
709 if (interval !== null)
710 return Request.poll.add(interval, url, { method: method, query: data }, cb);
711 else
712 return Request.request(url, { method: method, query: data })
713 .then(function(res) {
714 var json = null;
715 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
716 try { json = res.json() } catch(e) {}
717 cb(res.xhr, json, res.duration);
718 });
719 },
720
721 stop: function(entry) { return Request.poll.remove(entry) },
722 halt: function() { return Request.poll.stop() },
723 run: function() { return Request.poll.start() },
724
725 /* DOM manipulation */
726 dom: Class.singleton({
727 __name__: 'LuCI.DOM',
728
729 elem: function(e) {
730 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
731 },
732
733 parse: function(s) {
734 var elem;
735
736 try {
737 domParser = domParser || new DOMParser();
738 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
739 }
740 catch(e) {}
741
742 if (!elem) {
743 try {
744 dummyElem = dummyElem || document.createElement('div');
745 dummyElem.innerHTML = s;
746 elem = dummyElem.firstChild;
747 }
748 catch (e) {}
749 }
750
751 return elem || null;
752 },
753
754 matches: function(node, selector) {
755 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
756 return m ? m.call(node, selector) : false;
757 },
758
759 parent: function(node, selector) {
760 if (this.elem(node) && node.closest)
761 return node.closest(selector);
762
763 while (this.elem(node))
764 if (this.matches(node, selector))
765 return node;
766 else
767 node = node.parentNode;
768
769 return null;
770 },
771
772 append: function(node, children) {
773 if (!this.elem(node))
774 return null;
775
776 if (Array.isArray(children)) {
777 for (var i = 0; i < children.length; i++)
778 if (this.elem(children[i]))
779 node.appendChild(children[i]);
780 else if (children !== null && children !== undefined)
781 node.appendChild(document.createTextNode('' + children[i]));
782
783 return node.lastChild;
784 }
785 else if (typeof(children) === 'function') {
786 return this.append(node, children(node));
787 }
788 else if (this.elem(children)) {
789 return node.appendChild(children);
790 }
791 else if (children !== null && children !== undefined) {
792 node.innerHTML = '' + children;
793 return node.lastChild;
794 }
795
796 return null;
797 },
798
799 content: function(node, children) {
800 if (!this.elem(node))
801 return null;
802
803 var dataNodes = node.querySelectorAll('[data-idref]');
804
805 for (var i = 0; i < dataNodes.length; i++)
806 delete this.registry[dataNodes[i].getAttribute('data-idref')];
807
808 while (node.firstChild)
809 node.removeChild(node.firstChild);
810
811 return this.append(node, children);
812 },
813
814 attr: function(node, key, val) {
815 if (!this.elem(node))
816 return null;
817
818 var attr = null;
819
820 if (typeof(key) === 'object' && key !== null)
821 attr = key;
822 else if (typeof(key) === 'string')
823 attr = {}, attr[key] = val;
824
825 for (key in attr) {
826 if (!attr.hasOwnProperty(key) || attr[key] == null)
827 continue;
828
829 switch (typeof(attr[key])) {
830 case 'function':
831 node.addEventListener(key, attr[key]);
832 break;
833
834 case 'object':
835 node.setAttribute(key, JSON.stringify(attr[key]));
836 break;
837
838 default:
839 node.setAttribute(key, attr[key]);
840 }
841 }
842 },
843
844 create: function() {
845 var html = arguments[0],
846 attr = arguments[1],
847 data = arguments[2],
848 elem;
849
850 if (!(attr instanceof Object) || Array.isArray(attr))
851 data = attr, attr = null;
852
853 if (Array.isArray(html)) {
854 elem = document.createDocumentFragment();
855 for (var i = 0; i < html.length; i++)
856 elem.appendChild(this.create(html[i]));
857 }
858 else if (this.elem(html)) {
859 elem = html;
860 }
861 else if (html.charCodeAt(0) === 60) {
862 elem = this.parse(html);
863 }
864 else {
865 elem = document.createElement(html);
866 }
867
868 if (!elem)
869 return null;
870
871 this.attr(elem, attr);
872 this.append(elem, data);
873
874 return elem;
875 },
876
877 registry: {},
878
879 data: function(node, key, val) {
880 var id = node.getAttribute('data-idref');
881
882 /* clear all data */
883 if (arguments.length > 1 && key == null) {
884 if (id != null) {
885 node.removeAttribute('data-idref');
886 val = this.registry[id]
887 delete this.registry[id];
888 return val;
889 }
890
891 return null;
892 }
893
894 /* clear a key */
895 else if (arguments.length > 2 && key != null && val == null) {
896 if (id != null) {
897 val = this.registry[id][key];
898 delete this.registry[id][key];
899 return val;
900 }
901
902 return null;
903 }
904
905 /* set a key */
906 else if (arguments.length > 2 && key != null && val != null) {
907 if (id == null) {
908 do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
909 while (this.registry.hasOwnProperty(id));
910
911 node.setAttribute('data-idref', id);
912 this.registry[id] = {};
913 }
914
915 return (this.registry[id][key] = val);
916 }
917
918 /* get all data */
919 else if (arguments.length == 1) {
920 if (id != null)
921 return this.registry[id];
922
923 return null;
924 }
925
926 /* get a key */
927 else if (arguments.length == 2) {
928 if (id != null)
929 return this.registry[id][key];
930 }
931
932 return null;
933 },
934
935 bindClassInstance: function(node, inst) {
936 if (!(inst instanceof Class))
937 L.error('TypeError', 'Argument must be a class instance');
938
939 return this.data(node, '_class', inst);
940 },
941
942 findClassInstance: function(node) {
943 var inst = null;
944
945 do {
946 inst = this.data(node, '_class');
947 node = node.parentNode;
948 }
949 while (!(inst instanceof Class) && node != null);
950
951 return inst;
952 },
953
954 callClassMethod: function(node, method /*, ... */) {
955 var inst = this.findClassInstance(node);
956
957 if (inst == null || typeof(inst[method]) != 'function')
958 return null;
959
960 return inst[method].apply(inst, inst.varargs(arguments, 2));
961 }
962 }),
963
964 Class: Class,
965 Request: Request,
966
967 view: Class.extend({
968 __name__: 'LuCI.View',
969
970 __init__: function() {
971 var vp = document.getElementById('view');
972
973 L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
974
975 return Promise.resolve(this.load())
976 .then(L.bind(this.render, this))
977 .then(L.bind(function(nodes) {
978 var vp = document.getElementById('view');
979
980 L.dom.content(vp, nodes);
981 L.dom.append(vp, this.addFooter());
982 }, this)).catch(L.error);
983 },
984
985 load: function() {},
986 render: function() {},
987
988 handleSave: function(ev) {
989 var tasks = [];
990
991 document.getElementById('maincontent')
992 .querySelectorAll('.cbi-map').forEach(function(map) {
993 tasks.push(L.dom.callClassMethod(map, 'save'));
994 });
995
996 return Promise.all(tasks);
997 },
998
999 handleSaveApply: function(ev) {
1000 return this.handleSave(ev).then(function() {
1001 L.ui.changes.apply(true);
1002 });
1003 },
1004
1005 handleReset: function(ev) {
1006 var tasks = [];
1007
1008 document.getElementById('maincontent')
1009 .querySelectorAll('.cbi-map').forEach(function(map) {
1010 tasks.push(L.dom.callClassMethod(map, 'reset'));
1011 });
1012
1013 return Promise.all(tasks);
1014 },
1015
1016 addFooter: function() {
1017 var footer = E([]),
1018 mc = document.getElementById('maincontent');
1019
1020 if (mc.querySelector('.cbi-map')) {
1021 footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
1022 E('input', {
1023 'class': 'cbi-button cbi-button-apply',
1024 'type': 'button',
1025 'value': _('Save & Apply'),
1026 'click': L.bind(this.handleSaveApply, this)
1027 }), ' ',
1028 E('input', {
1029 'class': 'cbi-button cbi-button-save',
1030 'type': 'submit',
1031 'value': _('Save'),
1032 'click': L.bind(this.handleSave, this)
1033 }), ' ',
1034 E('input', {
1035 'class': 'cbi-button cbi-button-reset',
1036 'type': 'button',
1037 'value': _('Reset'),
1038 'click': L.bind(this.handleReset, this)
1039 })
1040 ]));
1041 }
1042
1043 return footer;
1044 }
1045 })
1046 });
1047
1048 XHR = Class.extend({
1049 __name__: 'LuCI.XHR',
1050 __init__: function() {
1051 if (window.console && console.debug)
1052 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
1053 },
1054
1055 _response: function(cb, res, json, duration) {
1056 if (this.active)
1057 cb(res, json, duration);
1058 delete this.active;
1059 },
1060
1061 get: function(url, data, callback, timeout) {
1062 this.active = true;
1063 L.get(url, data, this._response.bind(this, callback), timeout);
1064 },
1065
1066 post: function(url, data, callback, timeout) {
1067 this.active = true;
1068 L.post(url, data, this._response.bind(this, callback), timeout);
1069 },
1070
1071 cancel: function() { delete this.active },
1072 busy: function() { return (this.active === true) },
1073 abort: function() {},
1074 send_form: function() { L.error('InternalError', 'Not implemented') },
1075 });
1076
1077 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
1078 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
1079 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
1080 XHR.stop = Request.poll.remove.bind(Request.poll);
1081 XHR.halt = Request.poll.stop.bind(Request.poll);
1082 XHR.run = Request.poll.start.bind(Request.poll);
1083 XHR.running = Request.poll.active.bind(Request.poll);
1084
1085 window.XHR = XHR;
1086 window.LuCI = LuCI;
1087 })(window, document);