1 (function(window, document, undefined) {
4 /* Object.assign polyfill for IE */
5 if (typeof Object.assign !== 'function') {
6 Object.defineProperty(Object, 'assign', {
7 value: function assign(target, varArgs) {
9 throw new TypeError('Cannot convert undefined or null to object');
11 var to = Object(target);
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];
26 /* Promise.finally polyfill */
27 if (typeof Promise.prototype.finally !== 'function') {
28 Promise.prototype.finally = function(fn) {
29 var onFinally = function(cb) {
30 return Promise.resolve(fn.call(this)).then(cb);
34 function(result) { return onFinally.call(this, function() { return result }) },
35 function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
41 * Class declaration and inheritance helper
44 var toCamelCase = function(s) {
45 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
48 var superContext = null, Class = Object.assign(function() {}, {
49 extend: function(properties) {
51 __base__: { value: this.prototype },
52 __name__: { value: properties.__name__ || 'anonymous' }
55 var ClassConstructor = function() {
56 if (!(this instanceof ClassConstructor))
57 throw new TypeError('Constructor must not be called without "new"');
59 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
60 if (typeof(this.__init__) != 'function')
61 throw new TypeError('Class __init__ member is not a function');
63 this.__init__.apply(this, arguments)
66 this.super('__init__', arguments);
70 for (var key in properties)
71 if (!props[key] && properties.hasOwnProperty(key))
72 props[key] = { value: properties[key], writable: true };
74 ClassConstructor.prototype = Object.create(this.prototype, props);
75 ClassConstructor.prototype.constructor = ClassConstructor;
76 Object.assign(ClassConstructor, this);
77 ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
79 return ClassConstructor;
82 singleton: function(properties /*, ... */) {
83 return Class.extend(properties)
84 .instantiate(Class.prototype.varargs(arguments, 1));
87 instantiate: function(args) {
88 return new (Function.prototype.bind.apply(this,
89 Class.prototype.varargs(args, 0, null)))();
92 call: function(self, method) {
93 if (typeof(this.prototype[method]) != 'function')
94 throw new ReferenceError(method + ' is not defined in class');
96 return this.prototype[method].apply(self, self.varargs(arguments, 1));
99 isSubclass: function(_class) {
100 return (_class != null &&
101 typeof(_class) == 'function' &&
102 _class.prototype instanceof this);
106 varargs: function(args, offset /*, ... */) {
107 return Array.prototype.slice.call(arguments, 2)
108 .concat(Array.prototype.slice.call(args, offset));
111 super: function(key, callArgs) {
112 for (superContext = Object.getPrototypeOf(superContext ||
113 Object.getPrototypeOf(this));
114 superContext && !superContext.hasOwnProperty(key);
115 superContext = Object.getPrototypeOf(superContext)) { }
120 var res = superContext[key];
122 if (arguments.length > 1) {
123 if (typeof(res) != 'function')
124 throw new ReferenceError(key + ' is not a function in base class');
126 if (typeof(callArgs) != 'object')
127 callArgs = this.varargs(arguments, 1);
129 res = res.apply(this, callArgs);
137 toString: function() {
138 var s = '[' + this.constructor.displayName + ']', f = true;
139 for (var k in this) {
140 if (this.hasOwnProperty(k)) {
141 s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
145 return s + (f ? '' : '}');
152 * HTTP Request helper
155 var Headers = Class.extend({
156 __name__: 'LuCI.XHR.Headers',
157 __init__: function(xhr) {
158 var hdrs = this.headers = {};
159 xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
160 var m = /^([^:]+):(.*)$/.exec(line);
162 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
166 has: function(name) {
167 return this.headers.hasOwnProperty(String(name).toLowerCase());
170 get: function(name) {
171 var key = String(name).toLowerCase();
172 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
176 var Response = Class.extend({
177 __name__: 'LuCI.XHR.Response',
178 __init__: function(xhr, url, duration, headers, content) {
179 this.ok = (xhr.status >= 200 && xhr.status <= 299);
180 this.status = xhr.status;
181 this.statusText = xhr.statusText;
182 this.headers = (headers != null) ? headers : new Headers(xhr);
183 this.duration = duration;
187 if (content != null && typeof(content) == 'object') {
188 this.responseJSON = content;
189 this.responseText = null;
191 else if (content != null) {
192 this.responseJSON = null;
193 this.responseText = String(content);
196 this.responseJSON = null;
197 this.responseText = xhr.responseText;
201 clone: function(content) {
202 var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
205 copy.status = this.status;
206 copy.statusText = this.statusText;
212 if (this.responseJSON == null)
213 this.responseJSON = JSON.parse(this.responseText);
215 return this.responseJSON;
219 if (this.responseText == null && this.responseJSON != null)
220 this.responseText = JSON.stringify(this.responseJSON);
222 return this.responseText;
227 var requestQueue = [];
229 function isQueueableRequest(opt) {
233 if (opt.method != 'POST' || typeof(opt.content) != 'object')
236 if (opt.nobatch === true)
239 var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
241 return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
244 function flushRequestQueue() {
245 if (!requestQueue.length)
248 var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
251 for (var i = 0; i < requestQueue.length; i++) {
252 batch[i] = requestQueue[i];
253 reqopt.content[i] = batch[i][0].content;
256 requestQueue.length = 0;
258 Request.request(rpcBaseURL, reqopt).then(function(reply) {
259 var json = null, req = null;
261 try { json = reply.json() }
264 while ((req = batch.shift()) != null)
265 if (Array.isArray(json) && json.length)
266 req[2].call(reqopt, reply.clone(json.shift()));
268 req[1].call(reqopt, new Error('No related RPC reply'));
269 }).catch(function(error) {
272 while ((req = batch.shift()) != null)
273 req[1].call(reqopt, error);
277 var Request = Class.singleton({
278 __name__: 'LuCI.Request',
282 expandURL: function(url) {
283 if (!/^(?:[^/]+:)?\/\//.test(url))
284 url = location.protocol + '//' + location.host + url;
289 request: function(target, options) {
290 var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
291 opt = Object.assign({}, options, state),
294 callback = this.handleReadyStateChange;
296 return new Promise(function(resolveFn, rejectFn) {
297 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
298 opt.method = String(opt.method || 'GET').toUpperCase();
300 if ('query' in opt) {
301 var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
302 if (opt.query[k] != null) {
303 var v = (typeof(opt.query[k]) == 'object')
304 ? JSON.stringify(opt.query[k])
305 : String(opt.query[k]);
307 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
310 return encodeURIComponent(k);
315 switch (opt.method) {
319 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
323 if (content == null) {
325 contenttype = 'application/x-www-form-urlencoded';
332 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
334 if (isQueueableRequest(opt)) {
335 requestQueue.push([opt, rejectFn, resolveFn]);
336 requestAnimationFrame(flushRequestQueue);
340 if ('username' in opt && 'password' in opt)
341 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
343 opt.xhr.open(opt.method, opt.url, true);
345 opt.xhr.responseType = 'text';
347 if ('overrideMimeType' in opt.xhr)
348 opt.xhr.overrideMimeType('application/octet-stream');
350 if ('timeout' in opt)
351 opt.xhr.timeout = +opt.timeout;
353 if ('credentials' in opt)
354 opt.xhr.withCredentials = !!opt.credentials;
356 if (opt.content != null) {
357 switch (typeof(opt.content)) {
359 content = opt.content(xhr);
363 content = JSON.stringify(opt.content);
364 contenttype = 'application/json';
368 content = String(opt.content);
372 if ('headers' in opt)
373 for (var header in opt.headers)
374 if (opt.headers.hasOwnProperty(header)) {
375 if (header.toLowerCase() != 'content-type')
376 opt.xhr.setRequestHeader(header, opt.headers[header]);
378 contenttype = opt.headers[header];
381 if ('progress' in opt && 'upload' in opt.xhr)
382 opt.xhr.upload.addEventListener('progress', opt.progress);
384 if (contenttype != null)
385 opt.xhr.setRequestHeader('Content-Type', contenttype);
388 opt.xhr.send(content);
391 rejectFn.call(opt, e);
396 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
399 if (xhr.readyState !== 4)
402 if (xhr.status === 0 && xhr.statusText === '') {
403 rejectFn.call(this, new Error('XHR request aborted by browser'));
406 var response = new Response(
407 xhr, xhr.responseURL || this.url, Date.now() - this.start);
409 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
410 .then(resolveFn.bind(this, response))
411 .catch(rejectFn.bind(this));
415 get: function(url, options) {
416 return this.request(url, Object.assign({ method: 'GET' }, options));
419 post: function(url, data, options) {
420 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
423 addInterceptor: function(interceptorFn) {
424 if (typeof(interceptorFn) == 'function')
425 this.interceptors.push(interceptorFn);
426 return interceptorFn;
429 removeInterceptor: function(interceptorFn) {
430 var oldlen = this.interceptors.length, i = oldlen;
432 if (this.interceptors[i] === interceptorFn)
433 this.interceptors.splice(i, 1);
434 return (this.interceptors.length < oldlen);
438 add: function(interval, url, options, callback) {
439 if (isNaN(interval) || interval <= 0)
440 throw new TypeError('Invalid poll interval');
442 var ival = interval >>> 0,
443 opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
445 return Poll.add(function() {
446 return Request.request(url, options).then(function(res) {
451 callback(res, res.json(), res.duration);
454 callback(res, null, res.duration);
460 remove: function(entry) { return Poll.remove(entry) },
461 start: function() { return Poll.start() },
462 stop: function() { return Poll.stop() },
463 active: function() { return Poll.active() }
467 var Poll = Class.singleton({
468 __name__: 'LuCI.Poll',
472 add: function(fn, interval) {
473 if (interval == null || interval <= 0)
474 interval = window.L ? window.L.env.pollinterval : null;
476 if (isNaN(interval) || typeof(fn) != 'function')
477 throw new TypeError('Invalid argument to LuCI.Poll.add()');
479 for (var i = 0; i < this.queue.length; i++)
480 if (this.queue[i].fn === fn)
491 if (this.tick != null && !this.active())
497 remove: function(fn) {
498 if (typeof(fn) != 'function')
499 throw new TypeError('Invalid argument to LuCI.Poll.remove()');
501 var len = this.queue.length;
503 for (var i = len; i > 0; i--)
504 if (this.queue[i-1].fn === fn)
505 this.queue.splice(i-1, 1);
507 if (!this.queue.length && this.stop())
510 return (this.queue.length != len);
519 if (this.queue.length) {
520 this.timer = window.setInterval(this.step, 1000);
522 document.dispatchEvent(new CustomEvent('poll-start'));
532 document.dispatchEvent(new CustomEvent('poll-stop'));
533 window.clearInterval(this.timer);
540 for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
541 if ((Poll.tick % e.i) != 0)
549 Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
552 Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
556 return (this.timer != null);
561 var dummyElem = null,
563 originalCBIInit = null,
568 var LuCI = Class.extend({
570 __init__: function(env) {
572 document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
573 if (env.base_url == null || env.base_url == '')
574 env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
577 if (env.base_url == null)
578 this.error('InternalError', 'Cannot find url of luci.js');
580 Object.assign(this.env, env);
582 document.addEventListener('poll-start', function(ev) {
583 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
584 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
588 document.addEventListener('poll-stop', function(ev) {
589 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
590 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
594 var domReady = new Promise(function(resolveFn, rejectFn) {
595 document.addEventListener('DOMContentLoaded', resolveFn);
602 this.require('form'),
603 this.probeRPCBaseURL()
604 ]).then(this.setupDOM.bind(this)).catch(this.error);
606 originalCBIInit = window.cbi_init;
607 window.cbi_init = function() {};
610 raise: function(type, fmt /*, ...*/) {
612 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
615 if (type instanceof Error) {
617 stack = (e.stack || '').split(/\n/);
620 e.message = msg + ': ' + e.message;
623 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
624 e.name = type || 'Error';
627 if (window.console && console.debug)
633 error: function(type, fmt /*, ...*/) {
635 L.raise.apply(L, Array.prototype.slice.call(arguments));
638 var stack = (e.stack || '').split(/\n/).map(function(frame) {
639 frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
640 return frame ? ' ' + frame : '';
643 if (!/^ at /.test(stack[0]))
646 if (/\braise /.test(stack[0]))
649 if (/\berror /.test(stack[0]))
652 stack = stack.length ? '\n' + stack.join('\n') : '';
655 L.ui.showModal(e.name || _('Runtime error'),
656 E('pre', { 'class': 'alert-message error' }, e.message + stack));
658 L.dom.content(document.querySelector('#maincontent'),
659 E('pre', { 'class': 'alert-message error' }, e + stack));
665 bind: function(fn, self /*, ... */) {
666 return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
670 require: function(name, from) {
671 var L = this, url = null, from = from || [];
673 /* Class already loaded */
674 if (classes[name] != null) {
675 /* Circular dependency */
676 if (from.indexOf(name) != -1)
677 L.raise('DependencyError',
678 'Circular dependency: class "%s" depends on "%s"',
679 name, from.join('" which depends on "'));
681 return classes[name];
684 url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
685 from = [ name ].concat(from);
687 var compileClass = function(res) {
689 L.raise('NetworkError',
690 'HTTP error %d while loading class file "%s"', res.status, url);
692 var source = res.text(),
693 requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
694 strictmatch = /^use[ \t]+strict$/,
698 /* find require statements in source */
699 for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
700 var chr = source.charCodeAt(i);
705 else if (chr == 92) {
708 else if (chr == quote) {
709 var s = source.substring(off, i),
710 m = requirematch.exec(s);
713 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
714 depends.push(L.require(dep, from));
717 else if (!strictmatch.exec(s)) {
724 else if (quote == -1 && (chr == 34 || chr == 39)) {
730 /* load dependencies and instantiate class */
731 return Promise.all(depends).then(function(instances) {
732 var _factory, _class;
736 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
737 .format(args, source, res.url));
740 L.raise('SyntaxError', '%s\n in %s:%s',
741 error.message, res.url, error.lineNumber || '?');
744 _factory.displayName = toCamelCase(name + 'ClassFactory');
745 _class = _factory.apply(_factory, [window, document, L].concat(instances));
747 if (!Class.isSubclass(_class))
748 L.error('TypeError', '"%s" factory yields invalid constructor', name);
750 if (_class.displayName == 'AnonymousClass')
751 _class.displayName = toCamelCase(name + 'Class');
753 var ptr = Object.getPrototypeOf(L),
754 parts = name.split(/\./),
755 instance = new _class();
757 for (var i = 0; ptr && i < parts.length - 1; i++)
761 ptr[parts[i]] = instance;
763 classes[name] = instance;
769 /* Request class file */
770 classes[name] = Request.get(url, { cache: true }).then(compileClass);
772 return classes[name];
776 probeRPCBaseURL: function() {
777 if (rpcBaseURL == null) {
779 rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
784 if (rpcBaseURL == null) {
785 var rpcFallbackURL = this.url('admin/ubus');
787 rpcBaseURL = Request.get('/ubus/').then(function(res) {
788 return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
790 return (rpcBaseURL = rpcFallbackURL);
791 }).then(function(url) {
793 window.sessionStorage.setItem('rpcBaseURL', url);
801 return Promise.resolve(rpcBaseURL);
804 probeSystemFeatures: function() {
805 if (sysFeatures == null) {
807 sysFeatures = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
812 if (!this.isObject(sysFeatures)) {
813 sysFeatures = classes.rpc.declare({
815 method: 'getFeatures',
817 })().then(function(features) {
819 window.sessionStorage.setItem('sysFeatures', JSON.stringify(features));
823 sysFeatures = features;
829 return Promise.resolve(sysFeatures);
832 hasSystemFeature: function() {
833 var ft = sysFeatures[arguments[0]];
835 if (arguments.length == 2)
836 return this.isObject(ft) ? ft[arguments[1]] : null;
838 return (ft != null && ft != false);
841 setupDOM: function(res) {
848 rpcClass.setBaseURL(rpcBaseURL);
850 Request.addInterceptor(function(res) {
851 if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
856 L.ui.showModal(_('Session expired'), [
857 E('div', { class: 'alert-message warning' },
858 _('A new login is required since the authentication session expired.')),
859 E('div', { class: 'right' },
861 class: 'btn primary',
863 var loc = window.location;
864 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
869 throw 'Session expired';
872 return this.probeSystemFeatures().finally(this.initDOM);
875 initDOM: function() {
878 document.dispatchEvent(new CustomEvent('luci-loaded'));
883 /* URL construction helpers */
884 path: function(prefix, parts) {
885 var url = [ prefix || '' ];
887 for (var i = 0; i < parts.length; i++)
888 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
889 url.push('/', parts[i]);
891 if (url.length === 1)
898 return this.path(this.env.scriptname, arguments);
901 resource: function() {
902 return this.path(this.env.resource, arguments);
905 location: function() {
906 return this.path(this.env.scriptname, this.env.requestpath);
911 isObject: function(val) {
912 return (val != null && typeof(val) == 'object');
915 sortedKeys: function(obj, key, sortmode) {
916 if (obj == null || typeof(obj) != 'object')
919 return Object.keys(obj).map(function(e) {
920 var v = (key != null) ? obj[e][key] : e;
924 v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
925 function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
929 v = (v != null) ? +v : null;
934 }).filter(function(e) {
935 return (e[1] != null);
936 }).sort(function(a, b) {
937 return (a[1] > b[1]);
943 toArray: function(val) {
946 else if (Array.isArray(val))
948 else if (typeof(val) == 'object')
951 var s = String(val).trim();
956 return s.split(/\s+/);
960 /* HTTP resource fetching */
961 get: function(url, args, cb) {
962 return this.poll(null, url, args, cb, false);
965 post: function(url, args, cb) {
966 return this.poll(null, url, args, cb, true);
969 poll: function(interval, url, args, cb, post) {
970 if (interval !== null && interval <= 0)
971 interval = this.env.pollinterval;
973 var data = post ? { token: this.env.token } : null,
974 method = post ? 'POST' : 'GET';
976 if (!/^(?:\/|\S+:\/\/)/.test(url))
980 data = Object.assign(data || {}, args);
982 if (interval !== null)
983 return Request.poll.add(interval, url, { method: method, query: data }, cb);
985 return Request.request(url, { method: method, query: data })
986 .then(function(res) {
988 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
989 try { json = res.json() } catch(e) {}
990 cb(res.xhr, json, res.duration);
994 stop: function(entry) { return Poll.remove(entry) },
995 halt: function() { return Poll.stop() },
996 run: function() { return Poll.start() },
998 /* DOM manipulation */
999 dom: Class.singleton({
1000 __name__: 'LuCI.DOM',
1003 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
1006 parse: function(s) {
1010 domParser = domParser || new DOMParser();
1011 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
1017 dummyElem = dummyElem || document.createElement('div');
1018 dummyElem.innerHTML = s;
1019 elem = dummyElem.firstChild;
1024 return elem || null;
1027 matches: function(node, selector) {
1028 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
1029 return m ? m.call(node, selector) : false;
1032 parent: function(node, selector) {
1033 if (this.elem(node) && node.closest)
1034 return node.closest(selector);
1036 while (this.elem(node))
1037 if (this.matches(node, selector))
1040 node = node.parentNode;
1045 append: function(node, children) {
1046 if (!this.elem(node))
1049 if (Array.isArray(children)) {
1050 for (var i = 0; i < children.length; i++)
1051 if (this.elem(children[i]))
1052 node.appendChild(children[i]);
1053 else if (children !== null && children !== undefined)
1054 node.appendChild(document.createTextNode('' + children[i]));
1056 return node.lastChild;
1058 else if (typeof(children) === 'function') {
1059 return this.append(node, children(node));
1061 else if (this.elem(children)) {
1062 return node.appendChild(children);
1064 else if (children !== null && children !== undefined) {
1065 node.innerHTML = '' + children;
1066 return node.lastChild;
1072 content: function(node, children) {
1073 if (!this.elem(node))
1076 var dataNodes = node.querySelectorAll('[data-idref]');
1078 for (var i = 0; i < dataNodes.length; i++)
1079 delete this.registry[dataNodes[i].getAttribute('data-idref')];
1081 while (node.firstChild)
1082 node.removeChild(node.firstChild);
1084 return this.append(node, children);
1087 attr: function(node, key, val) {
1088 if (!this.elem(node))
1093 if (typeof(key) === 'object' && key !== null)
1095 else if (typeof(key) === 'string')
1096 attr = {}, attr[key] = val;
1099 if (!attr.hasOwnProperty(key) || attr[key] == null)
1102 switch (typeof(attr[key])) {
1104 node.addEventListener(key, attr[key]);
1108 node.setAttribute(key, JSON.stringify(attr[key]));
1112 node.setAttribute(key, attr[key]);
1117 create: function() {
1118 var html = arguments[0],
1119 attr = arguments[1],
1120 data = arguments[2],
1123 if (!(attr instanceof Object) || Array.isArray(attr))
1124 data = attr, attr = null;
1126 if (Array.isArray(html)) {
1127 elem = document.createDocumentFragment();
1128 for (var i = 0; i < html.length; i++)
1129 elem.appendChild(this.create(html[i]));
1131 else if (this.elem(html)) {
1134 else if (html.charCodeAt(0) === 60) {
1135 elem = this.parse(html);
1138 elem = document.createElement(html);
1144 this.attr(elem, attr);
1145 this.append(elem, data);
1152 data: function(node, key, val) {
1153 var id = node.getAttribute('data-idref');
1155 /* clear all data */
1156 if (arguments.length > 1 && key == null) {
1158 node.removeAttribute('data-idref');
1159 val = this.registry[id]
1160 delete this.registry[id];
1168 else if (arguments.length > 2 && key != null && val == null) {
1170 val = this.registry[id][key];
1171 delete this.registry[id][key];
1179 else if (arguments.length > 2 && key != null && val != null) {
1181 do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
1182 while (this.registry.hasOwnProperty(id));
1184 node.setAttribute('data-idref', id);
1185 this.registry[id] = {};
1188 return (this.registry[id][key] = val);
1192 else if (arguments.length == 1) {
1194 return this.registry[id];
1200 else if (arguments.length == 2) {
1202 return this.registry[id][key];
1208 bindClassInstance: function(node, inst) {
1209 if (!(inst instanceof Class))
1210 L.error('TypeError', 'Argument must be a class instance');
1212 return this.data(node, '_class', inst);
1215 findClassInstance: function(node) {
1219 inst = this.data(node, '_class');
1220 node = node.parentNode;
1222 while (!(inst instanceof Class) && node != null);
1227 callClassMethod: function(node, method /*, ... */) {
1228 var inst = this.findClassInstance(node);
1230 if (inst == null || typeof(inst[method]) != 'function')
1233 return inst[method].apply(inst, inst.varargs(arguments, 2));
1236 isEmpty: function(node, ignoreFn) {
1237 for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
1238 if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
1249 view: Class.extend({
1250 __name__: 'LuCI.View',
1252 __init__: function() {
1253 var vp = document.getElementById('view');
1255 L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
1257 return Promise.resolve(this.load())
1258 .then(L.bind(this.render, this))
1259 .then(L.bind(function(nodes) {
1260 var vp = document.getElementById('view');
1262 L.dom.content(vp, nodes);
1263 L.dom.append(vp, this.addFooter());
1264 }, this)).catch(L.error);
1267 load: function() {},
1268 render: function() {},
1270 handleSave: function(ev) {
1273 document.getElementById('maincontent')
1274 .querySelectorAll('.cbi-map').forEach(function(map) {
1275 tasks.push(L.dom.callClassMethod(map, 'save'));
1278 return Promise.all(tasks);
1281 handleSaveApply: function(ev) {
1282 return this.handleSave(ev).then(function() {
1283 L.ui.changes.apply(true);
1287 handleReset: function(ev) {
1290 document.getElementById('maincontent')
1291 .querySelectorAll('.cbi-map').forEach(function(map) {
1292 tasks.push(L.dom.callClassMethod(map, 'reset'));
1295 return Promise.all(tasks);
1298 addFooter: function() {
1300 mc = document.getElementById('maincontent');
1302 if (mc.querySelector('.cbi-map')) {
1303 footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
1305 'class': 'cbi-button cbi-button-apply',
1307 'value': _('Save & Apply'),
1308 'click': L.bind(this.handleSaveApply, this)
1311 'class': 'cbi-button cbi-button-save',
1314 'click': L.bind(this.handleSave, this)
1317 'class': 'cbi-button cbi-button-reset',
1319 'value': _('Reset'),
1320 'click': L.bind(this.handleReset, this)
1330 var XHR = Class.extend({
1331 __name__: 'LuCI.XHR',
1332 __init__: function() {
1333 if (window.console && console.debug)
1334 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
1337 _response: function(cb, res, json, duration) {
1339 cb(res, json, duration);
1343 get: function(url, data, callback, timeout) {
1345 L.get(url, data, this._response.bind(this, callback), timeout);
1348 post: function(url, data, callback, timeout) {
1350 L.post(url, data, this._response.bind(this, callback), timeout);
1353 cancel: function() { delete this.active },
1354 busy: function() { return (this.active === true) },
1355 abort: function() {},
1356 send_form: function() { L.error('InternalError', 'Not implemented') },
1359 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
1360 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
1361 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
1362 XHR.stop = Request.poll.remove.bind(Request.poll);
1363 XHR.halt = Request.poll.stop.bind(Request.poll);
1364 XHR.run = Request.poll.start.bind(Request.poll);
1365 XHR.running = Request.poll.active.bind(Request.poll);
1369 })(window, document);