luci-base: convert JavaScript code to ES6 style
authorPaul Donald <newtwen+github@gmail.com>
Fri, 6 Dec 2024 21:23:59 +0000 (22:23 +0100)
committerPaul Donald <newtwen+github@gmail.com>
Thu, 19 Dec 2024 23:42:19 +0000 (23:42 +0000)
Convert existing JavaScript code in LuCI base to utilize ES6 standard
features such as spread arguments, arrow functions, object method
declarations etc.

This makes the code somewhat easier to follow and slightly smaller.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/luci.js

index 0dcc09a51a5010a878940305ea54b4e74b3e59c7..4107a9019b97bfe0b47a2a134be7d3d6b85516bc 100644 (file)
@@ -9,54 +9,16 @@
  * The environment settings to use for the LuCI runtime.
  */
 
-(function(window, document, undefined) {
+((window, document, undefined) => {
        'use strict';
 
-       var env = {};
-
-       /* Object.assign polyfill for IE */
-       if (typeof Object.assign !== 'function') {
-               Object.defineProperty(Object, 'assign', {
-                       value: function assign(target, varArgs) {
-                               if (target == null)
-                                       throw new TypeError('Cannot convert undefined or null to object');
-
-                               var to = Object(target);
-
-                               for (var index = 1; index < arguments.length; index++)
-                                       if (arguments[index] != null)
-                                               for (var nextKey in arguments[index])
-                                                       if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
-                                                               to[nextKey] = arguments[index][nextKey];
-
-                               return to;
-                       },
-                       writable: true,
-                       configurable: true
-               });
-       }
-
-       /* 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) }) }
-                       );
-               };
-       }
+       const env = {};
 
        /*
         * Class declaration and inheritance helper
         */
 
-       var toCamelCase = function(s) {
-               return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
-       };
+       const toCamelCase = s => s.replace(/(?:^|[\. -])(.)/g, (m0, m1) => m1.toUpperCase());
 
        /**
         * @class baseclass
         * It provides simple means to create subclasses of given classes and
         * implements prototypal inheritance.
         */
-       var superContext = {}, classIndex = 0, Class = Object.assign(function() {}, {
+       const superContext = {};
+
+       let classIndex = 0;
+
+       const Class = Object.assign(function() {}, {
                /**
                 * Extends this base class with the properties described in
                 * `properties` and returns a new subclassed Class instance
                 * class to enable inheritance. The resulting value represents a
                 * class constructor and can be instantiated with `new`.
                 */
-               extend: function(properties) {
-                       var props = {
+               extend(properties) {
+                       const props = {
                                __id__: { value: classIndex },
                                __base__: { value: this.prototype },
-                               __name__: { value: properties.__name__ || 'anonymous' + classIndex++ }
+                               __name__: { value: properties.__name__ ?? `anonymous${classIndex++}` }
                        };
 
-                       var ClassConstructor = function() {
+                       const ClassConstructor = function() {
                                if (!(this instanceof ClassConstructor))
                                        throw new TypeError('Constructor must not be called without "new"');
 
                                }
                        };
 
-                       for (var key in properties)
+                       for (const key in properties)
                                if (!props[key] && properties.hasOwnProperty(key))
                                        props[key] = { value: properties[key], writable: true };
 
                        ClassConstructor.prototype = Object.create(this.prototype, props);
                        ClassConstructor.prototype.constructor = ClassConstructor;
                        Object.assign(ClassConstructor, this);
-                       ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
+                       ClassConstructor.displayName = toCamelCase(`${props.__name__.value}Class`);
 
                        return ClassConstructor;
                },
                 * properties with its prototype set to this base class to
                 * enable inheritance.
                 */
-               singleton: function(properties /*, ... */) {
-                       return Class.extend(properties)
-                               .instantiate(Class.prototype.varargs(arguments, 1));
+               singleton(properties, ...new_args) {
+                       return Class.extend(properties).instantiate(new_args);
                },
 
                /**
                 * An array of arbitrary values which will be passed as arguments
                 * to the constructor function.
                 *
-                * @param {...*} [new_args]
-                * Specifies arguments to be passed to the subclass constructor
-                * as-is in order to instantiate the new subclass.
-                *
                 * @returns {LuCI.baseclass}
                 * Returns a new LuCI.baseclass instance extended by the given
                 * properties with its prototype set to this base class to
                 * enable inheritance.
                 */
-               instantiate: function(args) {
-                       return new (Function.prototype.bind.apply(this,
-                               Class.prototype.varargs(args, 0, null)))();
+               instantiate(args) {
+                       return new (Function.prototype.bind.call(this, null, ...args))();
                },
 
                /* unused */
-               call: function(self, method) {
+               call(self, method, ...args) {
                        if (typeof(this.prototype[method]) != 'function')
-                               throw new ReferenceError(method + ' is not defined in class');
+                               throw new ReferenceError(`${method} is not defined in class`);
 
-                       return this.prototype[method].apply(self, self.varargs(arguments, 1));
+                       return this.prototype[method].call(self, method, ...args);
                },
 
                /**
                 * class or `false` if the given value is not a valid class or not
                 * a subclass of this class'.
                 */
-               isSubclass: function(classValue) {
-                       return (classValue != null &&
-                               typeof(classValue) == 'function' &&
-                               classValue.prototype instanceof this);
+               isSubclass(classValue) {
+                       return (typeof(classValue) == 'function' && classValue.prototype instanceof this);
                },
 
                prototype: {
                         * and the values extracted from the `args` array beginning with
                         * `offset`.
                         */
-                       varargs: function(args, offset /*, ... */) {
-                               return Array.prototype.slice.call(arguments, 2)
-                                       .concat(Array.prototype.slice.call(args, offset));
+                       varargs(args, offset, ...extra_args) {
+                               return extra_args.concat(Array.prototype.slice.call(args, offset));
                        },
 
                        /**
                         * This function has two signatures and is sensitive to the
                         * amount of arguments passed to it:
                         *  - `super('key')` -
-                        *    Returns the value of `key` when found within one of the
-                        *    parent classes.
+                        *      Returns the value of `key` when found within one of the
+                        *      parent classes.
                         *  - `super('key', ['arg1', 'arg2'])` -
-                        *    Calls the `key()` method with parameters `arg1` and `arg2`
-                        *    when found within one of the parent classes.
+                        *      Calls the `key()` method with parameters `arg1` and `arg2`
+                        *      when found within one of the parent classes.
                         *
                         * @memberof LuCI.baseclass
                         * @instance
                         * was found in the parent class chain or when the call to the
                         * superclass method returned `null`.
                         */
-                       super: function(key, callArgs) {
+                       super(key, ...callArgs) {
                                if (key == null)
                                        return null;
 
-                               var slotIdx = this.__id__ + '.' + key,
-                                   symStack = superContext[slotIdx],
-                                   protoCtx = null;
+                               const slotIdx = `${this.__id__}.${key}`;
+                               const symStack = superContext[slotIdx];
+                               let protoCtx = null;
 
                                for (protoCtx = Object.getPrototypeOf(symStack ? symStack[0] : Object.getPrototypeOf(this));
-                                    protoCtx != null && !protoCtx.hasOwnProperty(key);
-                                    protoCtx = Object.getPrototypeOf(protoCtx)) {}
+                                        protoCtx != null && !protoCtx.hasOwnProperty(key);
+                                        protoCtx = Object.getPrototypeOf(protoCtx)) {}
 
                                if (protoCtx == null)
                                        return null;
 
-                               var res = protoCtx[key];
+                               let res = protoCtx[key];
 
-                               if (arguments.length > 1) {
+                               if (callArgs.length > 0) {
                                        if (typeof(res) != 'function')
-                                               throw new ReferenceError(key + ' is not a function in base class');
+                                               throw new ReferenceError(`${key} is not a function in base class`);
 
-                                       if (typeof(callArgs) != 'object')
-                                               callArgs = this.varargs(arguments, 1);
+                                       if (Array.isArray(callArgs[0]) || LuCI.prototype.isArguments(callArgs[0]))
+                                               callArgs = callArgs[0];
 
                                        if (symStack)
                                                symStack.unshift(protoCtx);
                         * constructor functions `displayName` and describing the class
                         * members and their respective types.
                         */
-                       toString: function() {
-                               var s = '[' + this.constructor.displayName + ']', f = true;
-                               for (var k in this) {
+                       toString() {
+                               let s = `[${this.constructor.displayName}]`, f = true;
+                               for (const k in this) {
                                        if (this.hasOwnProperty(k)) {
-                                               s += (f ? ' {\n' : '') + '  ' + k + ': ' + typeof(this[k]) + '\n';
+                                               s += `${f ? ' {\n' : ''}  ${k}: ${typeof(this[k])}\n`;
                                                f = false;
                                        }
                                }
         * The `Headers` class is an internal utility class exposed in HTTP
         * response objects using the `response.headers` property.
         */
-       var Headers = Class.extend(/** @lends LuCI.headers.prototype */ {
+       const Headers = Class.extend(/** @lends LuCI.headers.prototype */ {
                __name__: 'LuCI.headers',
-               __init__: function(xhr) {
-                       var hdrs = this.headers = {};
-                       xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
-                               var m = /^([^:]+):(.*)$/.exec(line);
+               __init__(xhr) {
+                       const hdrs = this.headers = {};
+                       xhr.getAllResponseHeaders().split(/\r\n/).forEach(line => {
+                               const m = /^([^:]+):(.*)$/.exec(line);
                                if (m != null)
                                        hdrs[m[1].trim().toLowerCase()] = m[2].trim();
                        });
                 * @returns {boolean}
                 * Returns `true` if the header name is present, `false` otherwise
                 */
-               has: function(name) {
+               has(name) {
                        return this.headers.hasOwnProperty(String(name).toLowerCase());
                },
 
                 * @returns {string|null}
                 * The value of the given header name or `null` if the header isn't present.
                 */
-               get: function(name) {
-                       var key = String(name).toLowerCase();
+               get(name) {
+                       const key = String(name).toLowerCase();
                        return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
                }
        });
         *
         * The `Response` class is an internal utility class representing HTTP responses.
         */
-       var Response = Class.extend({
+       const Response = Class.extend({
                __name__: 'LuCI.response',
-               __init__: function(xhr, url, duration, headers, content) {
+               __init__(xhr, url, duration, headers, content) {
                        /**
                         * Describes whether the response is successful (status codes `200..299`) or not
                         * @instance
                 * @returns {LuCI.response}
                 * The cloned `Response` instance.
                 */
-               clone: function(content) {
-                       var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
+               clone(content) {
+                       const copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
 
                        copy.ok = this.ok;
                        copy.status = this.status;
                 * @returns {*}
                 * The parsed JSON data.
                 */
-               json: function() {
+               json() {
                        if (this.responseJSON == null)
                                this.responseJSON = JSON.parse(this.responseText);
 
                 * @returns {string}
                 * The response content.
                 */
-               text: function() {
+               text() {
                        if (this.responseText == null && this.responseJSON != null)
                                this.responseText = JSON.stringify(this.responseJSON);
 
                 * @returns {Blob}
                 * The response content as blob.
                 */
-               blob: function() {
+               blob() {
                        return this.responseBlob;
                }
        });
 
 
-       var requestQueue = [];
+       const requestQueue = [];
 
        function isQueueableRequest(opt) {
                if (!classes.rpc)
                if (opt.nobatch === true)
                        return false;
 
-               var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
+               const rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
 
                return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
        }
                if (!requestQueue.length)
                        return;
 
-               var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
-                   batch = [];
+               const reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }), batch = [];
 
-               for (var i = 0; i < requestQueue.length; i++) {
+               for (let 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;
+               Request.request(rpcBaseURL, reqopt).then(reply => {
+                       let json = null, req = null;
 
                        try { json = reply.json() }
                        catch(e) { }
                                        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;
+               }).catch(error => {
+                       let req = null;
 
                        while ((req = batch.shift()) != null)
                                req[1].call(reqopt, error);
         * The `Request` class allows initiating HTTP requests and provides utilities
         * for dealing with responses.
         */
-       var Request = Class.singleton(/** @lends LuCI.request.prototype */ {
+       const Request = Class.singleton(/** @lends LuCI.request.prototype */ {
                __name__: 'LuCI.request',
 
                interceptors: [],
                 * The absolute URL derived from the given one, or the original URL
                 * if it already was absolute.
                 */
-               expandURL: function(url) {
+               expandURL(url) {
                        if (!/^(?:[^/]+:)?\/\//.test(url))
-                               url = location.protocol + '//' + location.host + url;
+                               url = `${location.protocol}//${location.host}${url}`;
 
                        return url;
                },
                 * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
-               request: function(target, options) {
-                       return Promise.resolve(target).then((function(url) {
-                               var state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() },
-                                   opt = Object.assign({}, options, state),
-                                   content = null,
-                                   contenttype = null,
-                                   callback = this.handleReadyStateChange;
+               request(target, options) {
+                       return Promise.resolve(target).then(url => {
+                               const state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() };
+                               const opt = Object.assign({}, options, state);
+                               let content = null;
+                               let contenttype = null;
+                               const callback = this.handleReadyStateChange;
 
-                               return new Promise(function(resolveFn, rejectFn) {
+                               return new Promise((resolveFn, rejectFn) => {
                                        opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
-                                       opt.method = String(opt.method || 'GET').toUpperCase();
+                                       opt.method = String(opt.method ?? 'GET').toUpperCase();
 
                                        if ('query' in opt) {
-                                               var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
+                                               const q = (opt.query != null) ? Object.keys(opt.query).map(k => {
                                                        if (opt.query[k] != null) {
-                                                               var v = (typeof(opt.query[k]) == 'object')
+                                                               const v = (typeof(opt.query[k]) == 'object')
                                                                        ? JSON.stringify(opt.query[k])
                                                                        : String(opt.query[k]);
 
                                        else
                                                opt.xhr.open(opt.method, opt.url, true);
 
-                                       opt.xhr.responseType = opt.responseType || 'text';
+                                       opt.xhr.responseType = opt.responseType ?? 'text';
 
                                        if ('overrideMimeType' in opt.xhr)
                                                opt.xhr.overrideMimeType('application/octet-stream');
                                        }
 
                                        if ('headers' in opt)
-                                               for (var header in opt.headers)
+                                               for (const header in opt.headers)
                                                        if (opt.headers.hasOwnProperty(header)) {
                                                                if (header.toLowerCase() != 'content-type')
                                                                        opt.xhr.setRequestHeader(header, opt.headers[header]);
                                                rejectFn.call(opt, e);
                                        }
                                });
-                       }).bind(this));
+                       });
                },
 
-               handleReadyStateChange: function(resolveFn, rejectFn, ev) {
-                       var xhr = this.xhr,
-                           duration = Date.now() - this.start;
+               handleReadyStateChange(resolveFn, rejectFn, ev) {
+                       const xhr = this.xhr, duration = Date.now() - this.start;
 
                        if (xhr.readyState !== 4)
                                return;
                                        rejectFn.call(this, new Error('XHR request aborted by browser'));
                        }
                        else {
-                               var response = new Response(
-                                       xhr, xhr.responseURL || this.url, duration);
+                               const response = new Response(
+                                       xhr, xhr.responseURL ?? this.url, duration);
 
-                               Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
+                               Promise.all(Request.interceptors.map(fn => fn(response)))
                                        .then(resolveFn.bind(this, response))
                                        .catch(rejectFn.bind(this));
                        }
                 * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
-               get: function(url, options) {
+               get(url, options) {
                        return this.request(url, Object.assign({ method: 'GET' }, options));
                },
 
                 * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
-               post: function(url, data, options) {
+               post(url, data, options) {
                        return this.request(url, Object.assign({ method: 'POST', content: data }, options));
                },
 
                 * @returns {LuCI.request.interceptorFn}
                 * The registered function.
                 */
-               addInterceptor: function(interceptorFn) {
+               addInterceptor(interceptorFn) {
                        if (typeof(interceptorFn) == 'function')
                                this.interceptors.push(interceptorFn);
                        return interceptorFn;
                 * @returns {boolean}
                 * Returns `true` if any function has been removed, else `false`.
                 */
-               removeInterceptor: function(interceptorFn) {
-                       var oldlen = this.interceptors.length, i = oldlen;
+               removeInterceptor(interceptorFn) {
+                       const oldlen = this.interceptors.length;
+                       let i = oldlen;
                        while (i--)
                                if (this.interceptors[i] === interceptorFn)
                                        this.interceptors.splice(i, 1);
                         * @returns {function}
                         * Returns the internally created poll function.
                         */
-                       add: function(interval, url, options, callback) {
+                       add(interval, url, options, callback) {
                                if (isNaN(interval) || interval <= 0)
                                        throw new TypeError('Invalid poll interval');
 
-                               var ival = interval >>> 0,
-                                   opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
+                               const ival = interval >>> 0, opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
 
-                               var fn = function() {
-                                       return Request.request(url, opts).then(function(res) {
-                                               if (!Poll.active())
-                                                       return;
+                               const fn = () => Request.request(url, opts).then(res => {
+                                       if (!Poll.active())
+                                               return;
 
-                                               var res_json = null;
-                                               try {
-                                                       res_json = res.json();
-                                               }
-                                               catch (err) {}
+                                       let res_json = null;
+                                       try {
+                                               res_json = res.json();
+                                       }
+                                       catch (err) {}
 
-                                               callback(res, res_json, res.duration);
-                                       });
-                               };
+                                       callback(res, res_json, res.duration);
+                               });
 
                                return (Poll.add(fn, ival) ? fn : null);
                        },
                         * @returns {boolean}
                         * Returns `true` if any function has been removed, else `false`.
                         */
-                       remove: function(entry) { return Poll.remove(entry) },
+                       remove(entry) { return Poll.remove(entry) },
 
                        /**
                          * Alias for {@link LuCI.poll.start LuCI.poll.start()}.
                          * @instance
                          * @memberof LuCI.request.poll
                          */
-                       start: function() { return Poll.start() },
+                       start() { return Poll.start() },
 
                        /**
                          * Alias for {@link LuCI.poll.stop LuCI.poll.stop()}.
                          * @instance
                          * @memberof LuCI.request.poll
                          */
-                       stop: function() { return Poll.stop() },
+                       stop() { return Poll.stop() },
 
                        /**
                          * Alias for {@link LuCI.poll.active LuCI.poll.active()}.
                          * @instance
                          * @memberof LuCI.request.poll
                          */
-                       active: function() { return Poll.active() }
+                       active() { return Poll.active() }
                }
        });
 
         * as well as starting, stopping and querying the state of the polling
         * loop.
         */
-       var Poll = Class.singleton(/** @lends LuCI.poll.prototype */ {
+       const Poll = Class.singleton(/** @lends LuCI.poll.prototype */ {
                __name__: 'LuCI.poll',
 
                queue: [],
                 * Returns `true` if the function has been added or `false` if it
                 * already is registered.
                 */
-               add: function(fn, interval) {
+               add(fn, interval) {
                        if (interval == null || interval <= 0)
                                interval = env.pollinterval || null;
 
                        if (isNaN(interval) || typeof(fn) != 'function')
                                throw new TypeError('Invalid argument to LuCI.poll.add()');
 
-                       for (var i = 0; i < this.queue.length; i++)
+                       for (let i = 0; i < this.queue.length; i++)
                                if (this.queue[i].fn === fn)
                                        return false;
 
-                       var e = {
+                       const e = {
                                r: true,
                                i: interval >>> 0,
-                               fn: fn
+                               fn
                        };
 
                        this.queue.push(e);
                 * Returns `true` if the function has been removed or `false` if it
                 * wasn't found.
                 */
-               remove: function(fn) {
+               remove(fn) {
                        if (typeof(fn) != 'function')
                                throw new TypeError('Invalid argument to LuCI.poll.remove()');
 
-                       var len = this.queue.length;
+                       const len = this.queue.length;
 
-                       for (var i = len; i > 0; i--)
+                       for (let i = len; i > 0; i--)
                                if (this.queue[i-1].fn === fn)
                                        this.queue.splice(i-1, 1);
 
                 * Returns `true` if polling has been started (or if no functions
                 * where registered) or `false` when the polling loop already runs.
                 */
-               start: function() {
+               start() {
                        if (this.active())
                                return false;
 
                 * Returns `true` if polling has been stopped or `false` if it didn't
                 * run to begin with.
                 */
-               stop: function() {
+               stop() {
                        if (!this.active())
                                return false;
 
                },
 
                /* private */
-               step: function() {
-                       for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
+               step() {
+                       for (let i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
                                if ((Poll.tick % e.i) != 0)
                                        continue;
 
                 * @memberof LuCI.poll
                 * @returns {boolean} - Returns `true` if polling is active, else `false`.
                 */
-               active: function() {
+               active() {
                        return (this.timer != null);
                }
        });
         * To import the class in views, use `'require dom'`, to import it in
         * external JavaScript, use `L.require("dom").then(...)`.
         */
-       var DOM = Class.singleton(/** @lends LuCI.dom.prototype */ {
+       const DOM = Class.singleton(/** @lends LuCI.dom.prototype */ {
                __name__: 'LuCI.dom',
 
                /**
                 * @returns {boolean}
                 * Returns `true` if the value is a DOM `Node`, else `false`.
                 */
-               elem: function(e) {
+               elem(e) {
                        return (e != null && typeof(e) == 'object' && 'nodeType' in e);
                },
 
                 * Returns the first DOM `Node` extracted from the HTML fragment or
                 * `null` on parsing failures or if no element could be found.
                 */
-               parse: function(s) {
-                       var elem = null;
-
+               parse(s) {
                        try {
-                               domParser = domParser || new DOMParser();
-                               elem = domParser.parseFromString(s, 'text/html').body.firstChild;
+                               return domParser.parseFromString(s, 'text/html').body.firstChild;
+                       }
+                       catch(e) {
+                               return null;
                        }
-                       catch(e) {}
-
-                       return elem;
                },
 
                /**
                 * or `false` when the node argument is no valid DOM `Node` or the
                 * selector didn't match.
                 */
-               matches: function(node, selector) {
-                       var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
+               matches(node, selector) {
+                       const m = this.elem(node) ? (node.matches ?? node.msMatchesSelector) : null;
                        return m ? m.call(node, selector) : false;
                },
 
                 * `null` when the node argument is no valid DOM `Node` or the
                 * selector didn't match any parent.
                 */
-               parent: function(node, selector) {
+               parent(node, selector) {
                        if (this.elem(node) && node.closest)
                                return node.closest(selector);
 
                 * if either the `node` argument was no valid DOM `node` or if the
                 * `children` was `null` or didn't result in further DOM nodes.
                 */
-               append: function(node, children) {
+               append(node, children) {
                        if (!this.elem(node))
                                return null;
 
                        if (Array.isArray(children)) {
-                               for (var i = 0; i < children.length; i++)
+                               for (let i = 0; i < children.length; i++)
                                        if (this.elem(children[i]))
                                                node.appendChild(children[i]);
                                        else if (children !== null && children !== undefined)
-                                               node.appendChild(document.createTextNode('' + children[i]));
+                                               node.appendChild(document.createTextNode(`${children[i]}`));
 
                                return node.lastChild;
                        }
                                return node.appendChild(children);
                        }
                        else if (children !== null && children !== undefined) {
-                               node.innerHTML = '' + children;
+                               node.innerHTML = `${children}`;
                                return node.lastChild;
                        }
 
                 * if either the `node` argument was no valid DOM `node` or if the
                 * `children` was `null` or didn't result in further DOM nodes.
                 */
-               content: function(node, children) {
+               content(node, children) {
                        if (!this.elem(node))
                                return null;
 
-                       var dataNodes = node.querySelectorAll('[data-idref]');
+                       const dataNodes = node.querySelectorAll('[data-idref]');
 
-                       for (var i = 0; i < dataNodes.length; i++)
+                       for (let i = 0; i < dataNodes.length; i++)
                                delete this.registry[dataNodes[i].getAttribute('data-idref')];
 
                        while (node.firstChild)
                 * to the given `node` as-is, with the underlying `setAttribute()`
                 * call implicitly turning it into a string.
                 */
-               attr: function(node, key, val) {
+               attr(node, key, val) {
                        if (!this.elem(node))
                                return null;
 
-                       var attr = null;
+                       let attr = null;
 
                        if (typeof(key) === 'object' && key !== null)
                                attr = key;
                 * @returns {Node}
                 * Returns the newly created `Node`.
                 */
-               create: function() {
-                       var html = arguments[0],
-                           attr = arguments[1],
-                           data = arguments[2],
-                           elem;
+               create() {
+                       const html = arguments[0];
+                       let attr = arguments[1];
+                       let data = arguments[2];
+                       let elem;
 
                        if (!(attr instanceof Object) || Array.isArray(attr))
                                data = attr, attr = null;
 
                        if (Array.isArray(html)) {
                                elem = document.createDocumentFragment();
-                               for (var i = 0; i < html.length; i++)
+                               for (let i = 0; i < html.length; i++)
                                        elem.appendChild(this.create(html[i]));
                        }
                        else if (this.elem(html)) {
                 * number of arguments passed to it.
                 *
                 *  - `dom.data(node)` -
-                *     Fetches all data associated with the given node.
+                *       Fetches all data associated with the given node.
                 *  - `dom.data(node, key)` -
-                *     Fetches a specific key associated with the given node.
+                *       Fetches a specific key associated with the given node.
                 *  - `dom.data(node, key, val)` -
-                *     Sets a specific key to the given value associated with the
-                *     given node.
+                *       Sets a specific key to the given value associated with the
+                *       given node.
                 *  - `dom.data(node, null)` -
-                *     Clears any data associated with the node.
+                *       Clears any data associated with the node.
                 *  - `dom.data(node, key, null)` -
-                *     Clears the given key associated with the node.
+                *       Clears the given key associated with the node.
                 *
                 * @instance
                 * @memberof LuCI.dom
                 * Returns the get or set value, or `null` when no value could
                 * be found.
                 */
-               data: function(node, key, val) {
-                       if (!node || !node.getAttribute)
+               data(node, key, val) {
+                       if (!node?.getAttribute)
                                return null;
 
-                       var id = node.getAttribute('data-idref');
+                       let id = node.getAttribute('data-idref');
 
                        /* clear all data */
                        if (arguments.length > 1 && key == null) {
                 * @returns {Class}
                 * Returns the bound class instance.
                 */
-               bindClassInstance: function(node, inst) {
+               bindClassInstance(node, inst) {
                        if (!(inst instanceof Class))
                                LuCI.prototype.error('TypeError', 'Argument must be a class instance');
 
                 * Returns the founds class instance if any or `null` if no bound
                 * class could be found on the node itself or any of its parents.
                 */
-               findClassInstance: function(node) {
-                       var inst = null;
+               findClassInstance(node) {
+                       let inst = null;
 
                        do {
                                inst = this.data(node, '_class');
                 * no bound class instance could be found, or if the found
                 * instance didn't have the requested `method`.
                 */
-               callClassMethod: function(node, method /*, ... */) {
-                       var inst = this.findClassInstance(node);
+               callClassMethod(node, method, ...args) {
+                       const inst = this.findClassInstance(node);
 
-                       if (inst == null || typeof(inst[method]) != 'function')
+                       if (typeof(inst?.[method]) != 'function')
                                return null;
 
-                       return inst[method].apply(inst, inst.varargs(arguments, 2));
+                       return inst[method].call(inst, ...args);
                },
 
                /**
                 * any children node either has a `hidden` CSS class or a `false`
                 * result when testing it using the given `ignoreFn`.
                 */
-               isEmpty: function(node, ignoreFn) {
-                       for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
-                               if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
+               isEmpty(node, ignoreFn) {
+                       for (let child = node?.firstElementChild; child != null; child = child.nextElementSibling)
+                               if (!child.classList.contains('hidden') && !ignoreFn?.(child))
                                        return false;
 
                        return true;
         *
         * The `session` class provides various session related functionality.
         */
-       var Session = Class.singleton(/** @lends LuCI.session.prototype */ {
+       const Session = Class.singleton(/** @lends LuCI.session.prototype */ {
                __name__: 'LuCI.session',
 
                /**
                 * @returns {string}
                 * Returns the current session ID.
                 */
-               getID: function() {
-                       return env.sessionid || '00000000000000000000000000000000';
+               getID() {
+                       return env.sessionid ?? '00000000000000000000000000000000';
                },
 
                /**
                 * @returns {string|null}
                 * Returns the current session token or `null` if not logged in.
                 */
-               getToken: function() {
-                       return env.token || null;
+               getToken() {
+                       return env.token ?? null;
                },
 
                /**
                 * Returns the stored session data or `null` if the given key wasn't
                 * found.
                 */
-               getLocalData: function(key) {
+               getLocalData(key) {
                        try {
-                               var sid = this.getID(),
-                                   item = 'luci-session-store',
-                                   data = JSON.parse(window.sessionStorage.getItem(item));
+                               const sid = this.getID();
+                               const item = 'luci-session-store';
+                               let data = JSON.parse(window.sessionStorage.getItem(item));
 
                                if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
                                        data = {};
                 * @returns {boolean}
                 * Returns `true` if the data could be stored or `false` on error.
                 */
-               setLocalData: function(key, value) {
+               setLocalData(key, value) {
                        if (key == null)
                                return false;
 
                        try {
-                               var sid = this.getID(),
-                                   item = 'luci-session-store',
-                                   data = JSON.parse(window.sessionStorage.getItem(item));
+                               const sid = this.getID();
+                               const item = 'luci-session-store';
+                               let data = JSON.parse(window.sessionStorage.getItem(item));
 
                                if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
                                        data = {};
         * The `view` class forms the basis of views and provides a standard
         * set of methods to inherit from.
         */
-       var View = Class.extend(/** @lends LuCI.view.prototype */ {
+       const View = Class.extend(/** @lends LuCI.view.prototype */ {
                __name__: 'LuCI.view',
 
-               __init__: function() {
-                       var vp = document.getElementById('view');
+               __init__() {
+                       const vp = document.getElementById('view');
 
                        DOM.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
 
                        return Promise.resolve(this.load())
                                .then(LuCI.prototype.bind(this.render, this))
                                .then(LuCI.prototype.bind(function(nodes) {
-                                       var vp = document.getElementById('view');
+                                       const vp = document.getElementById('view');
 
                                        DOM.content(vp, nodes);
                                        DOM.append(vp, this.addFooter());
                 * @returns {*|Promise<*>}
                 * May return any value or a Promise resolving to any value.
                 */
-               load: function() {},
+               load() {},
 
                /**
                 * The render function is invoked after the
                 * Should return a DOM `Node` value or a `Promise` resolving
                 * to a `Node` value.
                 */
-               render: function() {},
+               render() {},
 
                /**
                 * The handleSave function is invoked when the user clicks
                 * returned promise runs to completion before the button
                 * is re-enabled.
                 */
-               handleSave: function(ev) {
-                       var tasks = [];
+               handleSave(ev) {
+                       const tasks = [];
 
                        document.getElementById('maincontent')
-                               .querySelectorAll('.cbi-map').forEach(function(map) {
+                               .querySelectorAll('.cbi-map').forEach(map => {
                                        tasks.push(DOM.callClassMethod(map, 'save'));
                                });
 
                 * returned promise runs to completion before the button
                 * is re-enabled.
                 */
-               handleSaveApply: function(ev, mode) {
-                       return this.handleSave(ev).then(function() {
+               handleSaveApply(ev, mode) {
+                       return this.handleSave(ev).then(() => {
                                classes.ui.changes.apply(mode == '0');
                        });
                },
                 * returned promise runs to completion before the button
                 * is re-enabled.
                 */
-               handleReset: function(ev) {
-                       var tasks = [];
+               handleReset(ev) {
+                       const tasks = [];
 
                        document.getElementById('maincontent')
-                               .querySelectorAll('.cbi-map').forEach(function(map) {
+                               .querySelectorAll('.cbi-map').forEach(map => {
                                        tasks.push(DOM.callClassMethod(map, 'reset'));
                                });
 
                 * or an empty `DocumentFragment` if all three `handle*()`
                 * methods are overwritten with `null`.
                 */
-               addFooter: function() {
-                       var footer = E([]),
-                           vp = document.getElementById('view'),
-                           hasmap = false,
-                           readonly = true;
+               addFooter() {
+                       const footer = E([]);
+                       const vp = document.getElementById('view');
+                       let hasmap = false;
+                       let readonly = true;
 
-                       vp.querySelectorAll('.cbi-map').forEach(function(map) {
-                               var m = DOM.findClassInstance(map);
+                       vp.querySelectorAll('.cbi-map').forEach(map => {
+                               const m = DOM.findClassInstance(map);
                                if (m) {
                                        hasmap = true;
 
                        if (!hasmap)
                                readonly = !LuCI.prototype.hasViewPermission();
 
-                       var saveApplyBtn = this.handleSaveApply ? new classes.ui.ComboButton('0', {
+                       const saveApplyBtn = this.handleSaveApply ? new classes.ui.ComboButton('0', {
                                0: [ _('Save & Apply') ],
                                1: [ _('Apply unchecked') ]
                        }, {
        });
 
 
-       var dummyElem = null,
-           domParser = null,
-           originalCBIInit = null,
-           rpcBaseURL = null,
-           sysFeatures = null,
-           preloadClasses = null;
+       const domParser = new DOMParser();
+       let originalCBIInit = null;
+       let rpcBaseURL = null;
+       let sysFeatures = null;
+       let preloadClasses = null;
 
        /* "preload" builtin classes to make the available via require */
-       var classes = {
+       const classes = {
                baseclass: Class,
                dom: DOM,
                poll: Poll,
                view: View
        };
 
-       var naturalCompare = new Intl.Collator(undefined, { numeric: true }).compare;
+       const naturalCompare = new Intl.Collator(undefined, { numeric: true }).compare;
 
-       var LuCI = Class.extend(/** @lends LuCI.prototype */ {
+       const LuCI = Class.extend(/** @lends LuCI.prototype */ {
                __name__: 'LuCI',
-               __init__: function(setenv) {
+               __init__(setenv) {
 
-                       document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
+                       document.querySelectorAll('script[src*="/luci.js"]').forEach(s => {
                                if (setenv.base_url == null || setenv.base_url == '') {
-                                       var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
+                                       const m = (s.getAttribute('src') ?? '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
                                        if (m) {
                                                setenv.base_url = m[1];
                                                setenv.resource_version = m[2];
 
                        Object.assign(env, setenv);
 
-                       var domReady = new Promise(function(resolveFn, rejectFn) {
+                       const domReady = new Promise((resolveFn, rejectFn) => {
                                document.addEventListener('DOMContentLoaded', resolveFn);
                        });
 
                        ]).then(this.setupDOM.bind(this)).catch(this.error);
 
                        originalCBIInit = window.cbi_init;
-                       window.cbi_init = function() {};
+                       window.cbi_init = () => {};
                },
 
                /**
                 * appended to the message and the type set to the given type
                 * argument or copied from the given error instance.
                 */
-               raise: function(type, fmt /*, ...*/) {
-                       var e = null,
-                           msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
-                           stack = null;
+               raise(type, fmt, ...args) {
+                       let e = null;
+                       const msg = fmt ? String.prototype.format.call(fmt, ...args) : null;
+                       const stack = [];
 
                        if (type instanceof Error) {
                                e = type;
 
                                if (msg)
-                                       e.message = msg + ': ' + e.message;
+                                       e.message = `${msg}: ${e.message}`;
                        }
                        else {
                                try { throw new Error('stacktrace') }
-                               catch (e2) { stack = (e2.stack || '').split(/\n/) }
+                               catch (e2) { stack.push(...(e2.stack ?? '').split(/\n/)) }
 
-                               e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
-                               e.name = type || 'Error';
+                               e = new (window[type ?? 'Error'] ?? Error)(msg ?? 'Unspecified error');
+                               e.name = type ?? 'Error';
                        }
 
-                       stack = (stack || []).map(function(frame) {
-                               frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
-                               return frame ? '  ' + frame : '';
-                       });
+                       for (let i = 0; i < stack.length; i++) {
+                               const frame = stack[i].replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
+                               stack[i] = frame ? `  ${frame}` : '';
+                       }
 
                        if (!/^  at /.test(stack[0]))
                                stack.shift();
                                stack.shift();
 
                        if (stack.length)
-                               e.message += '\n' + stack.join('\n');
+                               e.message += `\n${stack.join('\n')}`;
 
                        if (window.console && console.debug)
                                console.debug(e);
                 * appended to the message and the type set to the given type
                 * argument or copied from the given error instance.
                 */
-               error: function(type, fmt /*, ...*/) {
+               error(type, fmt /*, ...*/) {
                        try {
                                LuCI.prototype.raise.apply(LuCI.prototype,
                                        Array.prototype.slice.call(arguments));
                 * @returns {function}
                 * Returns the bound function.
                 */
-               bind: function(fn, self /*, ... */) {
-                       return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
+               bind(fn, self, ...args) {
+                       return Function.prototype.bind.call(fn, self, ...args);
                },
 
                /**
                 * @returns {Promise<LuCI.baseclass>}
                 * Returns the instantiated class.
                 */
-               require: function(name, from) {
-                       var L = this, url = null, from = from || [];
+               require(name, from = []) {
+                       const L = this;
+                       let url = null;
 
                        /* Class already loaded */
                        if (classes[name] != null) {
                                return Promise.resolve(classes[name]);
                        }
 
-                       url = '%s/%s.js%s'.format(env.base_url, name.replace(/\./g, '/'), (env.resource_version ? '?v=' + env.resource_version : ''));
+                       url = '%s/%s.js%s'.format(env.base_url, name.replace(/\./g, '/'), (env.resource_version ? `?v=${env.resource_version}` : ''));
                        from = [ name ].concat(from);
 
-                       var compileClass = function(res) {
+                       const compileClass = res => {
                                if (!res.ok)
                                        LuCI.prototype.raise('NetworkError',
                                                'HTTP error %d while loading class file "%s"', res.status, url);
 
-                               var source = res.text(),
-                                   requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
-                                   strictmatch = /^use[ \t]+strict$/,
-                                   depends = [],
-                                   args = '';
+                               const source = res.text();
+                               const requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/;
+                               const strictmatch = /^use[ \t]+strict$/;
+                               const depends = [];
+                               let args = '';
 
                                /* find require statements in source */
-                               for (var i = 0, off = -1, prev = -1, quote = -1, comment = -1, esc = false; i < source.length; i++) {
-                                       var chr = source.charCodeAt(i);
+                               for (let i = 0, off = -1, prev = -1, quote = -1, comment = -1, esc = false; i < source.length; i++) {
+                                       const chr = source.charCodeAt(i);
 
                                        if (esc) {
                                                esc = false;
                                                esc = true;
                                        }
                                        else if (chr == quote) {
-                                               var s = source.substring(off, i),
-                                                   m = requirematch.exec(s);
+                                               const 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, '_');
+                                                       const dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
                                                        depends.push(LuCI.prototype.require(dep, from));
-                                                       args += ', ' + as;
+                                                       args += `, ${as}`;
                                                }
                                                else if (!strictmatch.exec(s)) {
                                                        break;
                                }
 
                                /* load dependencies and instantiate class */
-                               return Promise.all(depends).then(function(instances) {
-                                       var _factory, _class;
+                               return Promise.all(depends).then(instances => {
+                                       let _factory, _class;
 
                                        try {
                                                _factory = eval(
                                        }
                                        catch (error) {
                                                LuCI.prototype.raise('SyntaxError', '%s\n  in %s:%s',
-                                                       error.message, res.url, error.lineNumber || '?');
+                                                       error.message, res.url, error.lineNumber ?? '?');
                                        }
 
-                                       _factory.displayName = toCamelCase(name + 'ClassFactory');
+                                       _factory.displayName = toCamelCase(`${name}ClassFactory`);
                                        _class = _factory.apply(_factory, [window, document, L].concat(instances));
 
                                        if (!Class.isSubclass(_class))
-                                           LuCI.prototype.error('TypeError', '"%s" factory yields invalid constructor', name);
+                                               LuCI.prototype.error('TypeError', '"%s" factory yields invalid constructor', name);
 
                                        if (_class.displayName == 'AnonymousClass')
-                                               _class.displayName = toCamelCase(name + 'Class');
+                                               _class.displayName = toCamelCase(`${name}Class`);
 
-                                       var ptr = Object.getPrototypeOf(L),
-                                           parts = name.split(/\./),
-                                           instance = new _class();
+                                       let ptr = Object.getPrototypeOf(L);
+                                       let idx = 0;
+                                       const parts = name.split(/\./);
+                                       const instance = new _class();
 
-                                       for (var i = 0; ptr && i < parts.length - 1; i++)
-                                               ptr = ptr[parts[i]];
+                                       while (ptr && idx < parts.length - 1)
+                                               ptr = ptr[parts[idx++]];
 
                                        if (ptr)
-                                               ptr[parts[i]] = instance;
+                                               ptr[parts[idx]] = instance;
 
                                        classes[name] = instance;
 
                },
 
                /* DOM setup */
-               probeRPCBaseURL: function() {
+               probeRPCBaseURL() {
                        if (rpcBaseURL == null)
                                rpcBaseURL = Session.getLocalData('rpcBaseURL');
 
                        if (rpcBaseURL == null) {
-                               var msg = {
+                               const msg = {
                                        jsonrpc: '2.0',
-                                       id:      'init',
+                                       id:       'init',
                                        method:  'list',
                                        params:  undefined
                                };
-                               var rpcFallbackURL = this.url('admin/ubus');
+                               const rpcFallbackURL = this.url('admin/ubus');
 
-                               rpcBaseURL = Request.post(env.ubuspath, msg, { nobatch: true }).then(function(res) {
-                                       return (rpcBaseURL = res.status == 200 ? env.ubuspath : rpcFallbackURL);
-                               }, function() {
-                                       return (rpcBaseURL = rpcFallbackURL);
-                               }).then(function(url) {
+                               rpcBaseURL = Request.post(env.ubuspath, msg, { nobatch: true }).then(res => rpcBaseURL = res.status == 200 ? env.ubuspath : rpcFallbackURL, () => rpcBaseURL = rpcFallbackURL).then(url => {
                                        Session.setLocalData('rpcBaseURL', url);
                                        return url;
                                });
                        return Promise.resolve(rpcBaseURL);
                },
 
-               probeSystemFeatures: function() {
+               probeSystemFeatures() {
                        if (sysFeatures == null)
                                sysFeatures = Session.getLocalData('features');
 
                                        object: 'luci',
                                        method: 'getFeatures',
                                        expect: { '': {} }
-                               })().then(function(features) {
+                               })().then(features => {
                                        Session.setLocalData('features', features);
                                        sysFeatures = features;
 
                        return Promise.resolve(sysFeatures);
                },
 
-               probePreloadClasses: function() {
+               probePreloadClasses() {
                        if (preloadClasses == null)
                                preloadClasses = Session.getLocalData('preload');
 
                                        method: 'list',
                                        params: [ 'path' ],
                                        expect: { 'entries': [] }
-                               })(this.fspath(this.resource('preload'))), []).then(function(entries) {
-                                       var classes = [];
+                               })(this.fspath(this.resource('preload'))), []).then(entries => {
+                                       const classes = [];
 
-                                       for (var i = 0; i < entries.length; i++) {
+                                       for (let i = 0; i < entries.length; i++) {
                                                if (entries[i].type != 'file')
                                                        continue;
 
-                                               var m = entries[i].name.match(/(.+)\.js$/);
+                                               const m = entries[i].name.match(/(.+)\.js$/);
 
                                                if (m)
                                                        classes.push('preload.%s'.format(m[1]));
                 * Return `null` when a sub-feature was queried for a feature which
                 * has no sub-features.
                 */
-               hasSystemFeature: function() {
-                       var ft = sysFeatures[arguments[0]];
+               hasSystemFeature() {
+                       const ft = sysFeatures[arguments[0]];
 
                        if (arguments.length == 2)
                                return this.isObject(ft) ? ft[arguments[1]] : null;
                },
 
                /* private */
-               notifySessionExpiry: function() {
+               notifySessionExpiry() {
                        Poll.stop();
 
                        classes.ui.showModal(_('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;
+                                               click() {
+                                                       const loc = window.location;
+                                                       window.location = `${loc.protocol}//${loc.host}${loc.pathname}${loc.search}`;
                                                }
                                        }, _('Log in…')))
                        ]);
                },
 
                /* private */
-               setupDOM: function(res) {
-                       var domEv = res[0],
-                           uiClass = res[1],
-                           rpcClass = res[2],
-                           formClass = res[3],
-                           rpcBaseURL = res[4];
+               setupDOM(res) {
+                       const domEv = res[0], uiClass = res[1], rpcClass = res[2], formClass = res[3], rpcBaseURL = res[4];
 
                        rpcClass.setBaseURL(rpcBaseURL);
 
-                       rpcClass.addInterceptor(function(msg, req) {
+                       rpcClass.addInterceptor((msg, req) => {
                                if (!LuCI.prototype.isObject(msg) ||
-                                   !LuCI.prototype.isObject(msg.error) ||
-                                   msg.error.code != -32002)
+                                       !LuCI.prototype.isObject(msg.error) ||
+                                       msg.error.code != -32002)
                                        return;
 
                                if (!LuCI.prototype.isObject(req) ||
-                                   (req.object == 'session' && req.method == 'access'))
+                                       (req.object == 'session' && req.method == 'access'))
                                        return;
 
                                return rpcClass.declare({
                                })('uci', 'luci', 'read').catch(LuCI.prototype.notifySessionExpiry);
                        });
 
-                       Request.addInterceptor(function(res) {
-                               var isDenied = false;
+                       Request.addInterceptor(res => {
+                               let isDenied = false;
 
                                if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
                                        isDenied = true;
                                LuCI.prototype.notifySessionExpiry();
                        });
 
-                       document.addEventListener('poll-start', function(ev) {
-                               uiClass.showIndicator('poll-status', _('Refreshing'), function(ev) {
+                       document.addEventListener('poll-start', ev => {
+                               uiClass.showIndicator('poll-status', _('Refreshing'), ev => {
                                        Request.poll.active() ? Request.poll.stop() : Request.poll.start();
                                });
                        });
 
-                       document.addEventListener('poll-stop', function(ev) {
+                       document.addEventListener('poll-stop', ev => {
                                uiClass.showIndicator('poll-status', _('Paused'), null, 'inactive');
                        });
 
                                this.probeSystemFeatures(),
                                this.probePreloadClasses()
                        ]).finally(LuCI.prototype.bind(function() {
-                               var tasks = [];
+                               const tasks = [];
 
                                if (Array.isArray(preloadClasses))
-                                       for (var i = 0; i < preloadClasses.length; i++)
+                                       for (let i = 0; i < preloadClasses.length; i++)
                                                tasks.push(this.require(preloadClasses[i]));
 
                                return Promise.all(tasks);
                },
 
                /* private */
-               initDOM: function() {
+               initDOM() {
                        originalCBIInit();
                        Poll.start();
                        document.dispatchEvent(new CustomEvent('luci-loaded'));
                 * @instance
                 * @memberof LuCI
                 */
-               env: env,
+               env,
 
                /**
                 * Construct an absolute filesystem path relative to the server
                 * @return {string}
                 * Return the joined path.
                 */
-               fspath: function(/* ... */) {
-                       var path = env.documentroot;
+               fspath() /* ... */{
+                       let path = env.documentroot;
 
-                       for (var i = 0; i < arguments.length; i++)
-                               path += '/' + arguments[i];
+                       for (let i = 0; i < arguments.length; i++)
+                               path += `/${arguments[i]}`;
 
-                       var p = path.replace(/\/+$/, '').replace(/\/+/g, '/').split(/\//),
-                           res = [];
+                       const p = path.replace(/\/+$/, '').replace(/\/+/g, '/').split(/\//), res = [];
 
-                       for (var i = 0; i < p.length; i++)
+                       for (let i = 0; i < p.length; i++)
                                if (p[i] == '..')
                                        res.pop();
                                else if (p[i] != '.')
                 * @return {string}
                 * Return the joined URL path.
                 */
-               path: function(prefix, parts) {
-                       var url = [ prefix || '' ];
+               path(prefix = '', parts) {
+                       const url = [ prefix ];
 
-                       for (var i = 0; i < parts.length; i++)
+                       for (let i = 0; i < parts.length; i++)
                                if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]) || /^\?[a-zA-Z0-9_.%=&;-]+$/.test(parts[i]))
                                        url.push(parts[i].startsWith('?') ? parts[i] : '/' + parts[i]);
 
                 * @return {string}
                 * Returns the resulting URL path.
                 */
-               url: function() {
+               url() {
                        return this.path(env.scriptname, arguments);
                },
 
                 * @return {string}
                 * Returns the resulting URL path.
                 */
-               resource: function() {
+               resource() {
                        return this.path(env.resource, arguments);
                },
 
                 * @return {string}
                 * Returns the resulting URL path.
                 */
-               media: function() {
+               media() {
                        return this.path(env.media, arguments);
                },
 
                 * @return {string}
                 * Returns the URL path to the current view.
                 */
-               location: function() {
+               location() {
                        return this.path(env.scriptname, env.requestpath);
                },
 
                 * Returns `true` if the given value is of type object and
                 * not `null`, else returns `false`.
                 */
-               isObject: function(val) {
+               isObject(val) {
                        return (val != null && typeof(val) == 'object');
                },
 
+               /**
+                * Tests whether the passed argument is a function arguments object.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {*} [val]
+                * The value to test
+                *
+                * @return {boolean}
+                * Returns `true` if the given value is a function arguments object,
+                * else returns `false`.
+                */
+               isArguments(val) {
+                       return (Object.prototype.toString.call(val) == '[object Arguments]');
+               },
+
                /**
                 * Return an array of sorted object keys, optionally sorted by
                 * a different key or a different sorting mode.
                 * @return {string[]}
                 * Returns an array containing the sorted keys of the given object.
                 */
-               sortedKeys: function(obj, key, sortmode) {
+               sortedKeys(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;
+                       return Object.keys(obj).map(e => {
+                               let 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;
+                                               (m0, m1) => (`000${m1.toLowerCase()}`).substr(-4)) : null;
                                        break;
 
                                case 'num':
                                }
 
                                return [ e, v ];
-                       }).filter(function(e) {
-                               return (e[1] != null);
-                       }).sort(function(a, b) {
-                               return naturalCompare(a[1], b[1]);
-                       }).map(function(e) {
-                               return e[0];
-                       });
+                       }).filter(e => e[1] != null).sort((a, b) => naturalCompare(a[1], b[1])).map(e => e[0]);
                },
 
                /**
                 * Returns 0 if both values are equal.
                 * Returns 1 if the first value is larger than the second one.
                 */
-               naturalCompare: naturalCompare,
+               naturalCompare,
 
                /**
                 * Converts the given value to an array using toArray() if needed,
                 * @return {Array<*>}
                 * Returns the resulting, numerically sorted array.
                 */
-               sortedArray: function(val) {
+               sortedArray(val) {
                        return this.toArray(val).sort(naturalCompare);
                },
 
                 * @return {Array<*>}
                 * Returns the resulting array.
                 */
-               toArray: function(val) {
+               toArray(val) {
                        if (val == null)
                                return [];
                        else if (Array.isArray(val))
                        else if (typeof(val) == 'object')
                                return [ val ];
 
-                       var s = String(val).trim();
+                       const s = String(val).trim();
 
                        if (s == '')
                                return [];
                 * Returns a new promise resolving either to the given input value or
                 * to the given default value on error.
                 */
-               resolveDefault: function(value, defvalue) {
-                       return Promise.resolve(value).catch(function() { return defvalue });
+               resolveDefault(value, defvalue) {
+                       return Promise.resolve(value).catch(() => defvalue);
                },
 
                /**
                 * @return {Promise<null>}
                 * Returns a promise resolving to `null` when concluded.
                 */
-               get: function(url, args, cb) {
+               get(url, args, cb) {
                        return this.poll(null, url, args, cb, false);
                },
 
                 * @return {Promise<null>}
                 * Returns a promise resolving to `null` when concluded.
                 */
-               post: function(url, args, cb) {
+               post(url, args, cb) {
                        return this.poll(null, url, args, cb, true);
                },
 
                 * be passed to {@link LuCI.poll.remove Poll.remove()} to remove the
                 * polling request.
                 */
-               poll: function(interval, url, args, cb, post) {
+               poll(interval, url, args, cb, post) {
                        if (interval !== null && interval <= 0)
                                interval = env.pollinterval;
 
-                       var data = post ? { token: env.token } : null,
-                           method = post ? 'POST' : 'GET';
+                       const data = Object.assign(post ? { token: env.token } : {}, args);
+                       const method = post ? 'POST' : 'GET';
 
                        if (!/^(?:\/|\S+:\/\/)/.test(url))
                                url = this.url(url);
 
-                       if (args != null)
-                               data = Object.assign(data || {}, args);
-
                        if (interval !== null)
-                               return Request.poll.add(interval, url, { method: method, query: data }, cb);
+                               return Request.poll.add(interval, url, { method, query: data }, cb);
                        else
-                               return Request.request(url, { method: method, query: data })
-                                       .then(function(res) {
-                                               var json = null;
+                               return Request.request(url, { method, query: data })
+                                       .then(res => {
+                                               let json = null;
                                                if (/^application\/json\b/.test(res.headers.get('Content-Type')))
                                                        try { json = res.json() } catch(e) {}
                                                cb(res.xhr, json, res.duration);
                 * permissions are granted or `true` if at least one required ACL
                 * group is granted with write permissions.
                 */
-               hasViewPermission: function() {
+               hasViewPermission() {
                        if (!this.isObject(env.nodespec) || !env.nodespec.satisfied)
-                           return null;
+                               return null;
 
                        return !env.nodespec.readonly;
                },
                 * Returns `true` when the function has been removed or `false` if
                 * it could not be found.
                 */
-               stop: function(entry) { return Poll.remove(entry) },
+               stop(entry) { return Poll.remove(entry) },
 
                /**
                 * Deprecated wrapper around {@link LuCI.poll.stop Poll.stop()}.
                 * Returns `true` when the polling loop has been stopped or `false`
                 * when it didn't run to begin with.
                 */
-               halt: function() { return Poll.stop() },
+               halt() { return Poll.stop() },
 
                /**
                 * Deprecated wrapper around {@link LuCI.poll.start Poll.start()}.
                 * Returns `true` when the polling loop has been started or `false`
                 * when it was already running.
                 */
-               run: function() { return Poll.start() },
+               run() { return Poll.start() },
 
                /**
                 * Legacy `L.dom` class alias. New view code should use `'require dom';`
                 * @memberof LuCI
                 * @deprecated
                 */
-               Poll: Poll,
+               Poll,
 
                /**
                 * Legacy `L.Request` class alias. New view code should use `'require request';`
                 * @memberof LuCI
                 * @deprecated
                 */
-               Request: Request,
+               Request,
 
                /**
                 * Legacy `L.Class` class alias. New view code should use `'require baseclass';`
                 * @memberof LuCI
                 * @deprecated
                 */
-               Class: Class
+               Class
        });
 
        /**
         * New code should use {@link LuCI.request} instead to implement HTTP
         * request handling.
         */
-       var XHR = Class.extend(/** @lends LuCI.xhr.prototype */ {
+       const XHR = Class.extend(/** @lends LuCI.xhr.prototype */ {
                __name__: 'LuCI.xhr',
-               __init__: function() {
+               __init__() {
                        if (window.console && console.debug)
                                console.debug('Direct use XHR() is deprecated, please use L.Request instead');
                },
 
-               _response: function(cb, res, json, duration) {
+               _response(cb, res, json, duration) {
                        if (this.active)
                                cb(res, json, duration);
                        delete this.active;
                 *
                 * @return {Promise<null>}
                 */
-               get: function(url, data, callback, timeout) {
+               get(url, data, callback, timeout) {
                        this.active = true;
                        LuCI.prototype.get(url, data, this._response.bind(this, callback), timeout);
                },
                 *
                 * @return {Promise<null>}
                 */
-               post: function(url, data, callback, timeout) {
+               post(url, data, callback, timeout) {
                        this.active = true;
                        LuCI.prototype.post(url, data, this._response.bind(this, callback), timeout);
                },
                 * @deprecated
                 * @memberof LuCI.xhr
                 */
-               cancel: function() { delete this.active },
+               cancel() { delete this.active },
 
                /**
                 * Checks the running state of the request.
                 * Returns `true` if the request is still running or `false` if it
                 * already completed.
                 */
-               busy: function() { return (this.active === true) },
+               busy() { return (this.active === true) },
 
                /**
                 * Ignored for backwards compatibility.
                 * @deprecated
                 * @memberof LuCI.xhr
                 */
-               abort: function() {},
+               abort() {},
 
                /**
                 * Existing for backwards compatibility.
                 * Throws an `InternalError` with the message `Not implemented`
                 * when invoked.
                 */
-               send_form: function() { LuCI.prototype.error('InternalError', 'Not implemented') },
+               send_form() { LuCI.prototype.error('InternalError', 'Not implemented') },
        });
 
-       XHR.get = function() { return LuCI.prototype.get.apply(LuCI.prototype, arguments) };
-       XHR.post = function() { return LuCI.prototype.post.apply(LuCI.prototype, arguments) };
-       XHR.poll = function() { return LuCI.prototype.poll.apply(LuCI.prototype, arguments) };
+       XHR.get = (...args) => LuCI.prototype.get.call(LuCI.prototype, ...args);
+       XHR.post = (...args) => LuCI.prototype.post.call(LuCI.prototype, ...args);
+       XHR.poll = (...args) => LuCI.prototype.poll.call(LuCI.prototype, ...args);
        XHR.stop = Request.poll.remove.bind(Request.poll);
        XHR.halt = Request.poll.stop.bind(Request.poll);
        XHR.run = Request.poll.start.bind(Request.poll);