(function(window, document, undefined) {
+ 'use strict';
+
/* Object.assign polyfill for IE */
if (typeof Object.assign !== 'function') {
Object.defineProperty(Object, 'assign', {
});
}
+ /* Promise.finally polyfill */
+ if (typeof Promise.prototype.finally !== 'function') {
+ Promise.prototype.finally = function(fn) {
+ var onFinally = function(cb) {
+ return Promise.resolve(fn.call(this)).then(cb);
+ };
+
+ return this.then(
+ function(result) { return onFinally.call(this, function() { return result }) },
+ function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
+ );
+ };
+ }
+
/*
* Class declaration and inheritance helper
*/
* HTTP Request helper
*/
- Headers = Class.extend({
+ var Headers = Class.extend({
__name__: 'LuCI.XHR.Headers',
__init__: function(xhr) {
var hdrs = this.headers = {};
}
});
- Response = Class.extend({
+ var Response = Class.extend({
__name__: 'LuCI.XHR.Response',
- __init__: function(xhr, url, duration) {
+ __init__: function(xhr, url, duration, headers, content) {
this.ok = (xhr.status >= 200 && xhr.status <= 299);
this.status = xhr.status;
this.statusText = xhr.statusText;
- this.responseText = xhr.responseText;
- this.headers = new Headers(xhr);
+ this.headers = (headers != null) ? headers : new Headers(xhr);
this.duration = duration;
this.url = url;
this.xhr = xhr;
+
+ if (content != null && typeof(content) == 'object') {
+ this.responseJSON = content;
+ this.responseText = null;
+ }
+ else if (content != null) {
+ this.responseJSON = null;
+ this.responseText = String(content);
+ }
+ else {
+ this.responseJSON = null;
+ this.responseText = xhr.responseText;
+ }
+ },
+
+ clone: function(content) {
+ var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
+
+ copy.ok = this.ok;
+ copy.status = this.status;
+ copy.statusText = this.statusText;
+
+ return copy;
},
json: function() {
- return JSON.parse(this.responseText);
+ if (this.responseJSON == null)
+ this.responseJSON = JSON.parse(this.responseText);
+
+ return this.responseJSON;
},
text: function() {
+ if (this.responseText == null && this.responseJSON != null)
+ this.responseText = JSON.stringify(this.responseJSON);
+
return this.responseText;
}
});
- Request = Class.singleton({
+
+ var requestQueue = [];
+
+ function isQueueableRequest(opt) {
+ if (!classes.rpc)
+ return false;
+
+ if (opt.method != 'POST' || typeof(opt.content) != 'object')
+ return false;
+
+ if (opt.nobatch === true)
+ return false;
+
+ var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
+
+ return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
+ }
+
+ function flushRequestQueue() {
+ if (!requestQueue.length)
+ return;
+
+ var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
+ batch = [];
+
+ for (var i = 0; i < requestQueue.length; i++) {
+ batch[i] = requestQueue[i];
+ reqopt.content[i] = batch[i][0].content;
+ }
+
+ requestQueue.length = 0;
+
+ Request.request(rpcBaseURL, reqopt).then(function(reply) {
+ var json = null, req = null;
+
+ try { json = reply.json() }
+ catch(e) { }
+
+ while ((req = batch.shift()) != null)
+ if (Array.isArray(json) && json.length)
+ req[2].call(reqopt, reply.clone(json.shift()));
+ else
+ req[1].call(reqopt, new Error('No related RPC reply'));
+ }).catch(function(error) {
+ var req = null;
+
+ while ((req = batch.shift()) != null)
+ req[1].call(reqopt, error);
+ });
+ }
+
+ var Request = Class.singleton({
__name__: 'LuCI.Request',
interceptors: [],
+ expandURL: function(url) {
+ if (!/^(?:[^/]+:)?\/\//.test(url))
+ url = location.protocol + '//' + location.host + url;
+
+ return url;
+ },
+
request: function(target, options) {
- var state = { xhr: new XMLHttpRequest(), url: target, start: Date.now() },
+ var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
opt = Object.assign({}, options, state),
content = null,
contenttype = null,
if (!opt.cache)
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
- if (!/^(?:[^/]+:)?\/\//.test(opt.url))
- opt.url = location.protocol + '//' + location.host + opt.url;
+ if (isQueueableRequest(opt)) {
+ requestQueue.push([opt, rejectFn, resolveFn]);
+ requestAnimationFrame(flushRequestQueue);
+ return;
+ }
if ('username' in opt && 'password' in opt)
opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
opt.xhr.open(opt.method, opt.url, true);
opt.xhr.responseType = 'text';
- opt.xhr.overrideMimeType('application/octet-stream');
+
+ if ('overrideMimeType' in opt.xhr)
+ opt.xhr.overrideMimeType('application/octet-stream');
if ('timeout' in opt)
opt.xhr.timeout = +opt.timeout;
break;
case 'object':
- content = JSON.stringify(opt.content);
- contenttype = 'application/json';
+ if (!(opt.content instanceof FormData)) {
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ }
+ else {
+ content = opt.content;
+ }
break;
default:
contenttype = opt.headers[header];
}
+ if ('progress' in opt && 'upload' in opt.xhr)
+ opt.xhr.upload.addEventListener('progress', opt.progress);
+
if (contenttype != null)
opt.xhr.setRequestHeader('Content-Type', contenttype);
},
handleReadyStateChange: function(resolveFn, rejectFn, ev) {
- var xhr = this.xhr;
+ var xhr = this.xhr,
+ duration = Date.now() - this.start;
if (xhr.readyState !== 4)
return;
if (xhr.status === 0 && xhr.statusText === '') {
- rejectFn.call(this, new Error('XHR request aborted by browser'));
+ if (duration >= this.timeout)
+ rejectFn.call(this, new Error('XHR request timed out'));
+ else
+ rejectFn.call(this, new Error('XHR request aborted by browser'));
}
else {
var response = new Response(
- xhr, xhr.responseURL || this.url, Date.now() - this.start);
+ xhr, xhr.responseURL || this.url, duration);
Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
.then(resolveFn.bind(this, response))
.catch(rejectFn.bind(this));
}
-
- try {
- xhr.abort();
- }
- catch(e) {}
},
get: function(url, options) {
return (this.interceptors.length < oldlen);
},
- poll: Class.singleton({
- __name__: 'LuCI.Request.Poll',
-
- queue: [],
-
+ poll: {
add: function(interval, url, options, callback) {
if (isNaN(interval) || interval <= 0)
throw new TypeError('Invalid poll interval');
- var e = {
- interval: interval,
- url: url,
- options: options,
- callback: callback
- };
+ var ival = interval >>> 0,
+ opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
- this.queue.push(e);
- return e;
+ return Poll.add(function() {
+ return Request.request(url, options).then(function(res) {
+ if (!Poll.active())
+ return;
+
+ try {
+ callback(res, res.json(), res.duration);
+ }
+ catch (err) {
+ callback(res, null, res.duration);
+ }
+ });
+ }, ival);
},
- remove: function(entry) {
- var oldlen = this.queue.length, i = oldlen;
+ remove: function(entry) { return Poll.remove(entry) },
+ start: function() { return Poll.start() },
+ stop: function() { return Poll.stop() },
+ active: function() { return Poll.active() }
+ }
+ });
- while (i--)
- if (this.queue[i] === entry) {
- delete this.queue[i].running;
- this.queue.splice(i, 1);
- }
+ var Poll = Class.singleton({
+ __name__: 'LuCI.Poll',
- if (!this.queue.length)
- this.stop();
+ queue: [],
- return (this.queue.length < oldlen);
- },
+ add: function(fn, interval) {
+ if (interval == null || interval <= 0)
+ interval = window.L ? window.L.env.pollinterval : null;
+
+ if (isNaN(interval) || typeof(fn) != 'function')
+ throw new TypeError('Invalid argument to LuCI.Poll.add()');
- start: function() {
- if (!this.queue.length || this.active())
+ for (var i = 0; i < this.queue.length; i++)
+ if (this.queue[i].fn === fn)
return false;
+ var e = {
+ r: true,
+ i: interval >>> 0,
+ fn: fn
+ };
+
+ this.queue.push(e);
+
+ if (this.tick != null && !this.active())
+ this.start();
+
+ return true;
+ },
+
+ remove: function(fn) {
+ if (typeof(fn) != 'function')
+ throw new TypeError('Invalid argument to LuCI.Poll.remove()');
+
+ var len = this.queue.length;
+
+ for (var i = len; i > 0; i--)
+ if (this.queue[i-1].fn === fn)
+ this.queue.splice(i-1, 1);
+
+ if (!this.queue.length && this.stop())
this.tick = 0;
+
+ return (this.queue.length != len);
+ },
+
+ start: function() {
+ if (this.active())
+ return false;
+
+ this.tick = 0;
+
+ if (this.queue.length) {
this.timer = window.setInterval(this.step, 1000);
this.step();
document.dispatchEvent(new CustomEvent('poll-start'));
- return true;
- },
+ }
- stop: function() {
- if (!this.active())
- return false;
+ return true;
+ },
- document.dispatchEvent(new CustomEvent('poll-stop'));
- window.clearInterval(this.timer);
- delete this.timer;
- delete this.tick;
- return true;
- },
+ stop: function() {
+ if (!this.active())
+ return false;
- step: function() {
- Request.poll.queue.forEach(function(e) {
- if ((Request.poll.tick % e.interval) != 0)
- return;
+ document.dispatchEvent(new CustomEvent('poll-stop'));
+ window.clearInterval(this.timer);
+ delete this.timer;
+ delete this.tick;
+ return true;
+ },
- if (e.running)
- return;
+ step: function() {
+ for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
+ if ((Poll.tick % e.i) != 0)
+ continue;
- var opts = Object.assign({}, e.options,
- { timeout: e.interval * 1000 - 5 });
+ if (!e.r)
+ continue;
- e.running = true;
- Request.request(e.url, opts)
- .then(function(res) {
- if (!e.running || !Request.poll.active())
- return;
+ e.r = false;
- try {
- e.callback(res, res.json(), res.duration);
- }
- catch (err) {
- e.callback(res, null, res.duration);
- }
- })
- .finally(function() { delete e.running });
- });
+ Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
+ }
- Request.poll.tick = (Request.poll.tick + 1) % Math.pow(2, 32);
- },
+ Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
+ },
- active: function() {
- return (this.timer != null);
- }
- })
+ active: function() {
+ return (this.timer != null);
+ }
});
var dummyElem = null,
domParser = null,
originalCBIInit = null,
+ rpcBaseURL = null,
+ sysFeatures = null,
classes = {};
- LuCI = Class.extend({
+ var LuCI = Class.extend({
__name__: 'LuCI',
__init__: function(env) {
- Object.assign(this.env, env);
- document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
+ document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
+ if (env.base_url == null || env.base_url == '') {
+ var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
+ if (m) {
+ env.base_url = m[1];
+ env.resource_version = m[2];
+ }
+ }
+ });
+
+ if (env.base_url == null)
+ this.error('InternalError', 'Cannot find url of luci.js');
+
+ Object.assign(this.env, env);
document.addEventListener('poll-start', function(ev) {
document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
});
});
+ var domReady = new Promise(function(resolveFn, rejectFn) {
+ document.addEventListener('DOMContentLoaded', resolveFn);
+ });
+
+ Promise.all([
+ domReady,
+ this.require('ui'),
+ this.require('rpc'),
+ this.require('form'),
+ this.probeRPCBaseURL()
+ ]).then(this.setupDOM.bind(this)).catch(this.error);
+
originalCBIInit = window.cbi_init;
window.cbi_init = function() {};
},
- error: function(type, fmt /*, ...*/) {
+ raise: function(type, fmt /*, ...*/) {
var e = null,
msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
stack = null;
if (type instanceof Error) {
e = type;
- stack = (e.stack || '').split(/\n/);
if (msg)
e.message = msg + ': ' + e.message;
}
else {
+ try { throw new Error('stacktrace') }
+ catch (e2) { stack = (e2.stack || '').split(/\n/) }
+
e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
e.name = type || 'Error';
+ }
- try { throw new Error('stacktrace') }
- catch (e2) { stack = (e2.stack || '').split(/\n/) }
+ stack = (stack || []).map(function(frame) {
+ frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
+ return frame ? ' ' + frame : '';
+ });
- /* IE puts the exception message into the first line */
- if (stack[0] == 'Error: stacktrace')
- stack.shift();
+ if (!/^ at /.test(stack[0]))
+ stack.shift();
- /* Pop L.error() invocation from stack */
+ if (/\braise /.test(stack[0]))
+ stack.shift();
+
+ if (/\berror /.test(stack[0]))
stack.shift();
- }
- /* Append shortened & beautified stacktrace to message */
- e.message += '\n' + stack.join('\n')
- .replace(/(.*?)@(.+):(\d+):(\d+)/g, ' at $1 ($2:$3:$4)');
+ if (stack.length)
+ e.message += '\n' + stack.join('\n');
if (window.console && console.debug)
console.debug(e);
throw e;
},
+ error: function(type, fmt /*, ...*/) {
+ try {
+ L.raise.apply(L, Array.prototype.slice.call(arguments));
+ }
+ catch (e) {
+ if (!e.reported) {
+ if (L.ui)
+ L.ui.addNotification(e.name || _('Runtime error'),
+ E('pre', {}, e.message), 'danger');
+ else
+ L.dom.content(document.querySelector('#maincontent'),
+ E('pre', { 'class': 'alert-message error' }, e.message));
+
+ e.reported = true;
+ }
+
+ throw e;
+ }
+ },
+
bind: function(fn, self /*, ... */) {
return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
},
if (classes[name] != null) {
/* Circular dependency */
if (from.indexOf(name) != -1)
- L.error('DependencyError',
+ L.raise('DependencyError',
'Circular dependency: class "%s" depends on "%s"',
name, from.join('" which depends on "'));
return classes[name];
}
- document.querySelectorAll('script[src$="/luci.js"]').forEach(function(s) {
- url = '%s/%s.js'.format(
- s.getAttribute('src').replace(/\/luci\.js$/, ''),
- name.replace(/\./g, '/'));
- });
-
- if (url == null)
- L.error('InternalError', 'Cannot find url of luci.js');
-
+ url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
from = [ name ].concat(from);
var compileClass = function(res) {
if (!res.ok)
- L.error('NetworkError',
+ L.raise('NetworkError',
'HTTP error %d while loading class file "%s"', res.status, url);
var source = res.text(),
- reqmatch = /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
+ requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
+ strictmatch = /^use[ \t]+strict$/,
depends = [],
args = '';
/* find require statements in source */
- for (var m = reqmatch.exec(source); m; m = reqmatch.exec(source)) {
- var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
- depends.push(L.require(dep, from));
- args += ', ' + as;
+ for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
+ var chr = source.charCodeAt(i);
+
+ if (esc) {
+ esc = false;
+ }
+ else if (chr == 92) {
+ esc = true;
+ }
+ else if (chr == quote) {
+ var s = source.substring(off, i),
+ m = requirematch.exec(s);
+
+ if (m) {
+ var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
+ depends.push(L.require(dep, from));
+ args += ', ' + as;
+ }
+ else if (!strictmatch.exec(s)) {
+ break;
+ }
+
+ off = -1;
+ quote = -1;
+ }
+ else if (quote == -1 && (chr == 34 || chr == 39)) {
+ off = i + 1;
+ quote = chr;
+ }
}
/* load dependencies and instantiate class */
return Promise.all(depends).then(function(instances) {
+ var _factory, _class;
+
try {
_factory = eval(
'(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
.format(args, source, res.url));
}
catch (error) {
- L.error('SyntaxError', '%s\n in %s:%s',
+ L.raise('SyntaxError', '%s\n in %s:%s',
error.message, res.url, error.lineNumber || '?');
}
for (var i = 0; ptr && i < parts.length - 1; i++)
ptr = ptr[parts[i]];
- if (!ptr)
- L.error('DependencyError',
- 'Parent "%s" for class "%s" is missing',
- parts.slice(0, i).join('.'), name);
+ if (ptr)
+ ptr[parts[i]] = instance;
- classes[name] = ptr[parts[i]] = instance;
+ classes[name] = instance;
return instance;
});
},
/* DOM setup */
- setupDOM: function(ev) {
- Promise.all([
- L.require('ui')
- ]).then(function() {
- Request.addInterceptor(function(res) {
- if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
- return;
-
- Request.poll.stop();
-
- L.ui.showModal(_('Session expired'), [
- E('div', { class: 'alert-message warning' },
- _('A new login is required since the authentication session expired.')),
- E('div', { class: 'right' },
- E('div', {
- class: 'btn primary',
- click: function() {
- var loc = window.location;
- window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
- }
- }, _('To login…')))
- ]);
-
- L.error('AuthenticationError', 'Session expired');
+ probeRPCBaseURL: function() {
+ if (rpcBaseURL == null) {
+ try {
+ rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
+ }
+ catch (e) { }
+ }
+
+ if (rpcBaseURL == null) {
+ var rpcFallbackURL = this.url('admin/ubus');
+
+ rpcBaseURL = Request.get('/ubus/').then(function(res) {
+ return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
+ }, function() {
+ return (rpcBaseURL = rpcFallbackURL);
+ }).then(function(url) {
+ try {
+ window.sessionStorage.setItem('rpcBaseURL', url);
+ }
+ catch (e) { }
+
+ return url;
+ });
+ }
+
+ return Promise.resolve(rpcBaseURL);
+ },
+
+ probeSystemFeatures: function() {
+ var sessionid = classes.rpc.getSessionID();
+
+ if (sysFeatures == null) {
+ try {
+ var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
+
+ if (this.isObject(data) && this.isObject(data[sessionid]))
+ sysFeatures = data[sessionid];
+ }
+ catch (e) {}
+ }
+
+ if (!this.isObject(sysFeatures)) {
+ sysFeatures = classes.rpc.declare({
+ object: 'luci',
+ method: 'getFeatures',
+ expect: { '': {} }
+ })().then(function(features) {
+ try {
+ var data = {};
+ data[sessionid] = features;
+
+ window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
+ }
+ catch (e) {}
+
+ sysFeatures = features;
+
+ return features;
});
+ }
+
+ return Promise.resolve(sysFeatures);
+ },
+
+ hasSystemFeature: function() {
+ var ft = sysFeatures[arguments[0]];
+
+ if (arguments.length == 2)
+ return this.isObject(ft) ? ft[arguments[1]] : null;
+
+ return (ft != null && ft != false);
+ },
- originalCBIInit();
- Request.poll.start();
+ notifySessionExpiry: function() {
+ Poll.stop();
+
+ L.ui.showModal(_('Session expired'), [
+ E('div', { class: 'alert-message warning' },
+ _('A new login is required since the authentication session expired.')),
+ E('div', { class: 'right' },
+ E('div', {
+ class: 'btn primary',
+ click: function() {
+ var loc = window.location;
+ window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
+ }
+ }, _('To login…')))
+ ]);
+
+ L.raise('SessionError', 'Login session is expired');
+ },
+
+ setupDOM: function(res) {
+ var domEv = res[0],
+ uiClass = res[1],
+ rpcClass = res[2],
+ formClass = res[3],
+ rpcBaseURL = res[4];
+
+ rpcClass.setBaseURL(rpcBaseURL);
- document.dispatchEvent(new CustomEvent('luci-loaded'));
- }).catch(function(error) {
- alert('LuCI class loading error:\n' + error);
+ rpcClass.addInterceptor(function(msg, req) {
+ if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
+ return;
+
+ if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+ return;
+
+ return rpcClass.declare({
+ 'object': 'session',
+ 'method': 'access',
+ 'params': [ 'scope', 'object', 'function' ],
+ 'expect': { access: true }
+ })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
});
+
+ Request.addInterceptor(function(res) {
+ var isDenied = false;
+
+ if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
+ isDenied = true;
+
+ if (!isDenied)
+ return;
+
+ L.notifySessionExpiry();
+ });
+
+ return this.probeSystemFeatures().finally(this.initDOM);
+ },
+
+ initDOM: function() {
+ originalCBIInit();
+ Poll.start();
+ document.dispatchEvent(new CustomEvent('luci-loaded'));
},
env: {},
},
+ /* Data helpers */
+ isObject: function(val) {
+ return (val != null && typeof(val) == 'object');
+ },
+
+ sortedKeys: function(obj, key, sortmode) {
+ if (obj == null || typeof(obj) != 'object')
+ return [];
+
+ return Object.keys(obj).map(function(e) {
+ var v = (key != null) ? obj[e][key] : e;
+
+ switch (sortmode) {
+ case 'addr':
+ v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
+ function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
+ break;
+
+ case 'num':
+ v = (v != null) ? +v : null;
+ break;
+ }
+
+ return [ e, v ];
+ }).filter(function(e) {
+ return (e[1] != null);
+ }).sort(function(a, b) {
+ return (a[1] > b[1]);
+ }).map(function(e) {
+ return e[0];
+ });
+ },
+
+ toArray: function(val) {
+ if (val == null)
+ return [];
+ else if (Array.isArray(val))
+ return val;
+ else if (typeof(val) == 'object')
+ return [ val ];
+
+ var s = String(val).trim();
+
+ if (s == '')
+ return [];
+
+ return s.split(/\s+/);
+ },
+
+
/* HTTP resource fetching */
get: function(url, args, cb) {
return this.poll(null, url, args, cb, false);
});
},
- stop: function(entry) { return Request.poll.remove(entry) },
- halt: function() { return Request.poll.stop() },
- run: function() { return Request.poll.start() },
+ stop: function(entry) { return Poll.remove(entry) },
+ halt: function() { return Poll.stop() },
+ run: function() { return Poll.start() },
/* DOM manipulation */
dom: Class.singleton({
if (!this.elem(node))
return null;
+ var dataNodes = node.querySelectorAll('[data-idref]');
+
+ for (var i = 0; i < dataNodes.length; i++)
+ delete this.registry[dataNodes[i].getAttribute('data-idref')];
+
while (node.firstChild)
node.removeChild(node.firstChild);
this.append(elem, data);
return elem;
+ },
+
+ registry: {},
+
+ data: function(node, key, val) {
+ var id = node.getAttribute('data-idref');
+
+ /* clear all data */
+ if (arguments.length > 1 && key == null) {
+ if (id != null) {
+ node.removeAttribute('data-idref');
+ val = this.registry[id]
+ delete this.registry[id];
+ return val;
+ }
+
+ return null;
+ }
+
+ /* clear a key */
+ else if (arguments.length > 2 && key != null && val == null) {
+ if (id != null) {
+ val = this.registry[id][key];
+ delete this.registry[id][key];
+ return val;
+ }
+
+ return null;
+ }
+
+ /* set a key */
+ else if (arguments.length > 2 && key != null && val != null) {
+ if (id == null) {
+ do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
+ while (this.registry.hasOwnProperty(id));
+
+ node.setAttribute('data-idref', id);
+ this.registry[id] = {};
+ }
+
+ return (this.registry[id][key] = val);
+ }
+
+ /* get all data */
+ else if (arguments.length == 1) {
+ if (id != null)
+ return this.registry[id];
+
+ return null;
+ }
+
+ /* get a key */
+ else if (arguments.length == 2) {
+ if (id != null)
+ return this.registry[id][key];
+ }
+
+ return null;
+ },
+
+ bindClassInstance: function(node, inst) {
+ if (!(inst instanceof Class))
+ L.error('TypeError', 'Argument must be a class instance');
+
+ return this.data(node, '_class', inst);
+ },
+
+ findClassInstance: function(node) {
+ var inst = null;
+
+ do {
+ inst = this.data(node, '_class');
+ node = node.parentNode;
+ }
+ while (!(inst instanceof Class) && node != null);
+
+ return inst;
+ },
+
+ callClassMethod: function(node, method /*, ... */) {
+ var inst = this.findClassInstance(node);
+
+ if (inst == null || typeof(inst[method]) != 'function')
+ return null;
+
+ return inst[method].apply(inst, inst.varargs(arguments, 2));
+ },
+
+ isEmpty: function(node, ignoreFn) {
+ for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
+ if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
+ return false;
+
+ return true;
}
}),
+ Poll: Poll,
Class: Class,
- Request: Request
+ Request: Request,
+
+ view: Class.extend({
+ __name__: 'LuCI.View',
+
+ __init__: function() {
+ var vp = document.getElementById('view');
+
+ L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
+
+ return Promise.resolve(this.load())
+ .then(L.bind(this.render, this))
+ .then(L.bind(function(nodes) {
+ var vp = document.getElementById('view');
+
+ L.dom.content(vp, nodes);
+ L.dom.append(vp, this.addFooter());
+ }, this)).catch(L.error);
+ },
+
+ load: function() {},
+ render: function() {},
+
+ handleSave: function(ev) {
+ var tasks = [];
+
+ document.getElementById('maincontent')
+ .querySelectorAll('.cbi-map').forEach(function(map) {
+ tasks.push(L.dom.callClassMethod(map, 'save'));
+ });
+
+ return Promise.all(tasks);
+ },
+
+ handleSaveApply: function(ev) {
+ return this.handleSave(ev).then(function() {
+ L.ui.changes.apply(true);
+ });
+ },
+
+ handleReset: function(ev) {
+ var tasks = [];
+
+ document.getElementById('maincontent')
+ .querySelectorAll('.cbi-map').forEach(function(map) {
+ tasks.push(L.dom.callClassMethod(map, 'reset'));
+ });
+
+ return Promise.all(tasks);
+ },
+
+ addFooter: function() {
+ var footer = E([]);
+
+ if (this.handleSaveApply || this.handleSave || this.handleReset) {
+ footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
+ this.handleSaveApply ? E('button', {
+ 'class': 'cbi-button cbi-button-apply',
+ 'click': L.ui.createHandlerFn(this, 'handleSaveApply')
+ }, [ _('Save & Apply') ]) : '', ' ',
+ this.handleSave ? E('button', {
+ 'class': 'cbi-button cbi-button-save',
+ 'click': L.ui.createHandlerFn(this, 'handleSave')
+ }, [ _('Save') ]) : '', ' ',
+ this.handleReset ? E('button', {
+ 'class': 'cbi-button cbi-button-reset',
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, [ _('Reset') ]) : ''
+ ]));
+ }
+
+ return footer;
+ }
+ })
});
- XHR = Class.extend({
+ var XHR = Class.extend({
__name__: 'LuCI.XHR',
__init__: function() {
if (window.console && console.debug)