luci-base: convert JavaScript code to ES6 style es6 6597/head
authorJo-Philipp Wich <jo@mein.io>
Thu, 31 Aug 2023 15:03:49 +0000 (17:03 +0200)
committerJo-Philipp Wich <jo@mein.io>
Tue, 26 Sep 2023 07:36:25 +0000 (09:36 +0200)
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/form.js
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/htdocs/luci-static/resources/rpc.js
modules/luci-base/htdocs/luci-static/resources/uci.js
modules/luci-base/htdocs/luci-static/resources/ui.js
modules/luci-base/htdocs/luci-static/resources/validation.js

index 66ba0302087f3692546904223c6ead85c5a0669f..815fb5afbf5f51783a978f929947844981a82cf0 100644 (file)
@@ -5,32 +5,33 @@
 'require dom';
 'require baseclass';
 
-var scope = this;
+const scope = this;
 
-var callSessionAccess = rpc.declare({
+const callSessionAccess = rpc.declare({
        object: 'session',
        method: 'access',
        params: [ 'scope', 'object', 'function' ],
        expect: { 'access': false }
 });
 
-var CBIJSONConfig = baseclass.extend({
-       __init__: function(data) {
+const CBIJSONConfig = baseclass.extend({
+       __init__(data) {
                data = Object.assign({}, data);
 
                this.data = {};
 
-               var num_sections = 0,
-                   section_ids = [];
+               let num_sections = 0;
+               const section_ids = [];
 
-               for (var sectiontype in data) {
+               for (const sectiontype in data) {
                        if (!data.hasOwnProperty(sectiontype))
                                continue;
 
                        if (Array.isArray(data[sectiontype])) {
-                               for (var i = 0, index = 0; i < data[sectiontype].length; i++) {
-                                       var item = data[sectiontype][i],
-                                           anonymous, name;
+                               for (let i = 0, index = 0; i < data[sectiontype].length; i++) {
+                                       const item = data[sectiontype][i];
+                                       let anonymous;
+                                       let name;
 
                                        if (!L.isObject(item))
                                                continue;
@@ -68,8 +69,8 @@ var CBIJSONConfig = baseclass.extend({
                }
 
                section_ids.sort(L.bind(function(a, b) {
-                       var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999,
-                           indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
+                       const indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999;
+                       const indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
 
                        if (indexA != indexB)
                                return (indexA - indexB);
@@ -77,19 +78,19 @@ var CBIJSONConfig = baseclass.extend({
                        return L.naturalCompare(a, b);
                }, this));
 
-               for (var i = 0; i < section_ids.length; i++)
+               for (let i = 0; i < section_ids.length; i++)
                        this.data[section_ids[i]]['.index'] = i;
        },
 
-       load: function() {
+       load() {
                return Promise.resolve(this.data);
        },
 
-       save: function() {
+       save() {
                return Promise.resolve();
        },
 
-       get: function(config, section, option) {
+       get(config, section, option) {
                if (section == null)
                        return null;
 
@@ -99,7 +100,7 @@ var CBIJSONConfig = baseclass.extend({
                if (!this.data.hasOwnProperty(section))
                        return null;
 
-               var value = this.data[section][option];
+               const value = this.data[section][option];
 
                if (Array.isArray(value))
                        return value;
@@ -110,7 +111,7 @@ var CBIJSONConfig = baseclass.extend({
                return null;
        },
 
-       set: function(config, section, option, value) {
+       set(config, section, option, value) {
                if (section == null || option == null || option.charAt(0) == '.')
                        return;
 
@@ -125,35 +126,36 @@ var CBIJSONConfig = baseclass.extend({
                        this.data[section][option] = String(value);
        },
 
-       unset: function(config, section, option) {
+       unset(config, section, option) {
                return this.set(config, section, option, null);
        },
 
-       sections: function(config, sectiontype, callback) {
-               var rv = [];
+       sections(config, sectiontype, callback) {
+               const rv = [];
 
-               for (var section_id in this.data)
+               for (const section_id in this.data)
                        if (sectiontype == null || this.data[section_id]['.type'] == sectiontype)
                                rv.push(this.data[section_id]);
 
                rv.sort(function(a, b) { return a['.index'] - b['.index'] });
 
                if (typeof(callback) == 'function')
-                       for (var i = 0; i < rv.length; i++)
+                       for (let i = 0; i < rv.length; i++)
                                callback.call(this, rv[i], rv[i]['.name']);
 
                return rv;
        },
 
-       add: function(config, sectiontype, sectionname) {
-               var num_sections_type = 0, next_index = 0;
+       add(config, sectiontype, sectionname) {
+               let num_sections_type = 0;
+               let next_index = 0;
 
-               for (var name in this.data) {
+               for (const name in this.data) {
                        num_sections_type += (this.data[name]['.type'] == sectiontype);
                        next_index = Math.max(next_index, this.data[name]['.index']);
                }
 
-               var section_id = sectionname || sectiontype + num_sections_type;
+               const section_id = sectionname ?? (sectiontype + num_sections_type);
 
                if (!this.data.hasOwnProperty(section_id)) {
                        this.data[section_id] = {
@@ -167,16 +169,16 @@ var CBIJSONConfig = baseclass.extend({
                return section_id;
        },
 
-       remove: function(config, section) {
+       remove(config, section) {
                if (this.data.hasOwnProperty(section))
                        delete this.data[section];
        },
 
-       resolveSID: function(config, section_id) {
+       resolveSID(config, section_id) {
                return section_id;
        },
 
-       move: function(config, section_id1, section_id2, after) {
+       move(config, section_id1, section_id2, after) {
                return uci.move.apply(this, [config, section_id1, section_id2, after]);
        }
 });
@@ -194,10 +196,10 @@ var CBIJSONConfig = baseclass.extend({
  *
  * This class is private and not directly accessible by user code.
  */
-var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.prototype */ {
-       __init__: function(title, description) {
-               this.title = title || '';
-               this.description = description || '';
+const CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.prototype */ {
+       __init__(title, description) {
+               this.title = title ?? '';
+               this.description = description ?? '';
                this.children = [];
        },
 
@@ -207,7 +209,7 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
         * @param {AbstractElement} obj
         * The form element to add.
         */
-       append: function(obj) {
+       append(obj) {
                this.children.push(obj);
        },
 
@@ -225,10 +227,10 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
         * if any parsed values are not meeting the validation constraints of their
         * respective elements.
         */
-       parse: function() {
-               var args = arguments;
+       parse() {
+               const args = arguments;
                this.children.forEach(function(child) {
-                       child.parse.apply(child, args);
+                       child.parse(...args);
                });
        },
 
@@ -243,33 +245,32 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
         * May return a DOM Node or a promise resolving to a DOM node containing
         * the form element's markup, including the markup of any child elements.
         */
-       render: function() {
+       render() {
                L.error('InternalError', 'Not implemented');
        },
 
        /** @private */
-       loadChildren: function(/* ... */) {
-               var tasks = [];
+       loadChildren(...args) /* ... */{
+               const tasks = [];
 
                if (Array.isArray(this.children))
-                       for (var i = 0; i < this.children.length; i++)
+                       for (let i = 0; i < this.children.length; i++)
                                if (!this.children[i].disable)
-                                       tasks.push(this.children[i].load.apply(this.children[i], arguments));
+                                       tasks.push(this.children[i].load(...args));
 
                return Promise.all(tasks);
        },
 
        /** @private */
-       renderChildren: function(tab_name /*, ... */) {
-               var tasks = [],
-                   index = 0;
+       renderChildren(tab_name, ...args) {
+               const tasks = [];
+               let index = 0;
 
                if (Array.isArray(this.children))
-                       for (var i = 0; i < this.children.length; i++)
+                       for (let i = 0; i < this.children.length; i++)
                                if (tab_name === null || this.children[i].tab === tab_name)
                                        if (!this.children[i].disable)
-                                               tasks.push(this.children[i].render.apply(
-                                                       this.children[i], this.varargs(arguments, 1, index++)));
+                                               tasks.push(this.children[i].render(index++, ...args));
 
                return Promise.all(tasks);
        },
@@ -283,17 +284,17 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
         * @returns {string}
         * The cleaned input string with HTML tags removed.
         */
-       stripTags: function(s) {
+       stripTags(s) {
                if (typeof(s) == 'string' && !s.match(/[<>]/))
                        return s;
 
-               var x = dom.elem(s) ? s : dom.parse('<div>' + s + '</div>');
+               const x = dom.elem(s) ? s : dom.parse(`<div>${s}</div>`);
 
                x.querySelectorAll('br').forEach(function(br) {
                        x.replaceChild(document.createTextNode('\n'), br);
                });
 
-               return (x.textContent || x.innerText || '').replace(/([ \t]*\n)+/g, '\n');
+               return (x.textContent ?? x.innerText ?? '').replace(/([ \t]*\n)+/g, '\n');
        },
 
        /**
@@ -322,13 +323,13 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
         * The formatted title string or `null` if the property did not exist or
         * was neither a string nor a function.
         */
-       titleFn: function(attr /*, ... */) {
-               var s = null;
+       titleFn(attr, ...args) {
+               let s = null;
 
                if (typeof(this[attr]) == 'function')
-                       s = this[attr].apply(this, this.varargs(arguments, 1));
+                       s = this[attr](...args);
                else if (typeof(this[attr]) == 'string')
-                       s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
+                       s = args.length ? this[attr].format(...args) : this[attr];
 
                if (s != null)
                        s = this.stripTags(String(s)).trim();
@@ -367,9 +368,9 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
  * paragraph below the form title and before the actual form contents.
  * If omitted, the corresponding paragraph element will not be rendered.
  */
-var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
-       __init__: function(config /*, ... */) {
-               this.super('__init__', this.varargs(arguments, 1));
+const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
+       __init__(config, ...args) {
+               this.super('__init__', args);
 
                this.config = config;
                this.parsechain = [ config ];
@@ -420,13 +421,13 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * @returns {NodeList}
         * Returns a (possibly empty) DOM `NodeList` containing the found DOM nodes.
         */
-       findElements: function(/* ... */) {
-               var q = null;
+       findElements(...args) /* ... */{
+               let q = null;
 
-               if (arguments.length == 1)
-                       q = arguments[0];
-               else if (arguments.length == 2)
-                       q = '[%s="%s"]'.format(arguments[0], arguments[1]);
+               if (args.length == 1)
+                       q = args[0];
+               else if (args.length == 2)
+                       q = '[%s="%s"]'.format(args[0], args[1]);
                else
                        L.error('InternalError', 'Expecting one or two arguments to findElements()');
 
@@ -463,8 +464,8 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * @returns {Node|null}
         * Returns the first found DOM node or `null` if no element matched.
         */
-       findElement: function(/* ... */) {
-               var res = this.findElements.apply(this, arguments);
+       findElement(...args) /* ... */{
+               const res = this.findElements(...args);
                return res.length ? res[0] : null;
        },
 
@@ -481,7 +482,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * The additional UCI configuration file to tie to the map. If the given
         * config already is in the list of required files, it will be ignored.
         */
-       chain: function(config) {
+       chain(config) {
                if (this.parsechain.indexOf(config) == -1)
                        this.parsechain.push(config);
        },
@@ -513,11 +514,11 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * @returns {LuCI.form.AbstractSection}
         * Returns the instantiated section class instance.
         */
-       section: function(cbiClass /*, ... */) {
+       section(cbiClass, ...args) {
                if (!CBIAbstractSection.isSubclass(cbiClass))
                        L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
 
-               var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
+               const obj = cbiClass.instantiate([this, ...args]);
                this.append(obj);
                return obj;
        },
@@ -535,12 +536,12 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * to load or if any of the child elements load functions rejected with
         * an error.
         */
-       load: function() {
-               var doCheckACL = (!(this instanceof CBIJSONMap) && this.readonly == null),
-                   loadTasks = [ doCheckACL ? callSessionAccess('uci', this.config, 'write') : true ],
-                   configs = this.parsechain || [ this.config ];
+       load() {
+               const doCheckACL = (!(this instanceof CBIJSONMap) && this.readonly == null);
+               const loadTasks = [ doCheckACL ? callSessionAccess('uci', this.config, 'write') : true ];
+               const configs = this.parsechain ?? [ this.config ];
 
-               loadTasks.push.apply(loadTasks, configs.map(L.bind(function(config, i) {
+               loadTasks.push(...configs.map(L.bind(function(config, i) {
                        return i ? L.resolveDefault(this.data.load(config)) : this.data.load(config);
                }, this)));
 
@@ -565,11 +566,11 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * input values. The returned promise is rejected if any parsed values are
         * not meeting the validation constraints of their respective elements.
         */
-       parse: function() {
-               var tasks = [];
+       parse() {
+               const tasks = [];
 
                if (Array.isArray(this.children))
-                       for (var i = 0; i < this.children.length; i++)
+                       for (let i = 0; i < this.children.length; i++)
                                tasks.push(this.children[i].parse());
 
                return Promise.all(tasks);
@@ -595,7 +596,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * The returned promise is rejected if any step of the save operation
         * failed.
         */
-       save: function(cb, silent) {
+       save(cb, silent) {
                this.checkDepends();
 
                return this.parse()
@@ -625,7 +626,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * Returns a promise resolving to the toplevel form DOM node once the
         * re-rendering is complete.
         */
-       reset: function() {
+       reset() {
                return this.renderContents();
        },
 
@@ -636,13 +637,13 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * Returns a promise resolving to the toplevel form DOM node once the
         * rendering is complete.
         */
-       render: function() {
+       render() {
                return this.load().then(this.renderContents.bind(this));
        },
 
        /** @private */
-       renderContents: function() {
-               var mapEl = this.root || (this.root = E('div', {
+       renderContents() {
+               const mapEl = (this.root ??= E('div', {
                        'id': 'cbi-%s'.format(this.config),
                        'class': 'cbi-map',
                        'cbi-dependency-check': L.bind(this.checkDepends, this)
@@ -651,7 +652,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
                dom.bindClassInstance(mapEl, this);
 
                return this.renderChildren(null).then(L.bind(function(nodes) {
-                       var initialRender = !mapEl.firstChild;
+                       const initialRender = !mapEl.firstChild;
 
                        dom.content(mapEl, null);
 
@@ -676,9 +677,9 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
 
                        this.checkDepends();
 
-                       var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
+                       const tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
 
-                       for (var i = 0; i < tabGroups.length; i++)
+                       for (let i = 0; i < tabGroups.length; i++)
                                ui.tabs.initTabGroup(tabGroups[i].childNodes);
 
                        return mapEl;
@@ -704,13 +705,16 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * first item and the corresponding UCI section ID as second item.
         * Returns `null` if the option could not be found.
         */
-       lookupOption: function(name, section_id, config_name) {
-               var id, elem, sid, inst;
+       lookupOption(name, section_id, config_name) {
+               let id;
+               let elem;
+               let sid;
+               let inst;
 
                if (name.indexOf('.') > -1)
                        id = 'cbid.%s'.format(name);
                else
-                       id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
+                       id = 'cbid.%s.%s.%s'.format(config_name ?? this.config, section_id, name);
 
                elem = this.findElement('data-field', id);
                sid  = elem ? id.split(/\./)[2] : null;
@@ -720,32 +724,32 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
        },
 
        /** @private */
-       checkDepends: function(ev, n) {
-               var changed = false;
+       checkDepends(ev, n) {
+               let changed = false;
 
-               for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
+               for (let i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
                        if (s.checkDepends(ev, n))
                                changed = true;
 
-               if (changed && (n || 0) < 10)
-                       this.checkDepends(ev, (n || 10) + 1);
+               if (changed && (n ?? 0) < 10)
+                       this.checkDepends(ev, (n ?? 10) + 1);
 
                ui.tabs.updateTabs(ev, this.root);
        },
 
        /** @private */
-       isDependencySatisfied: function(depends, config_name, section_id) {
-               var def = false;
+       isDependencySatisfied(depends, config_name, section_id) {
+               let def = false;
 
                if (!Array.isArray(depends) || !depends.length)
                        return true;
 
-               for (var i = 0; i < depends.length; i++) {
-                       var istat = true,
-                           reverse = depends[i]['!reverse'],
-                           contains = depends[i]['!contains'];
+               for (let i = 0; i < depends.length; i++) {
+                       let istat = true;
+                       const reverse = depends[i]['!reverse'];
+                       const contains = depends[i]['!contains'];
 
-                       for (var dep in depends[i]) {
+                       for (const dep in depends[i]) {
                                if (dep == '!reverse' || dep == '!contains') {
                                        continue;
                                }
@@ -754,10 +758,10 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
                                        istat = false;
                                }
                                else {
-                                       var res = this.lookupOption(dep, section_id, config_name),
-                                           val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
+                                       const res = this.lookupOption(dep, section_id, config_name);
+                                       const val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
 
-                                       var equal = contains
+                                       const equal = contains
                                                ? isContained(val, depends[i][dep])
                                                : isEqual(val, depends[i][dep]);
 
@@ -800,9 +804,9 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
  * paragraph below the form title and before the actual form contents.
  * If omitted, the corresponding paragraph element will not be rendered.
  */
-var CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
-       __init__: function(data /*, ... */) {
-               this.super('__init__', this.varargs(arguments, 1, 'json'));
+const CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
+       __init__(data, ...args) {
+               this.super('__init__', [ 'json', ...args ]);
 
                this.config = 'json';
                this.parsechain = [ 'json' ];
@@ -824,9 +828,9 @@ var CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
  *
  * This class is private and not directly accessible by user code.
  */
-var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractSection.prototype */ {
-       __init__: function(map, sectionType /*, ... */) {
-               this.super('__init__', this.varargs(arguments, 2));
+const CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractSection.prototype */ {
+       __init__(map, sectionType, ...args) {
+               this.super('__init__', args);
 
                this.sectiontype = sectionType;
                this.map = map;
@@ -861,7 +865,7 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * Returns an array of UCI section IDs covered by this form element.
         * The sections will be rendered in the same order as the returned array.
         */
-       cfgsections: function() {
+       cfgsections() {
                L.error('InternalError', 'Not implemented');
        },
 
@@ -884,7 +888,7 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * Returns `true` when the given UCI section ID should be handled and
         * `false` when it should be ignored.
         */
-       filter: function(section_id) {
+       filter(section_id) {
                return true;
        },
 
@@ -899,15 +903,15 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * been loaded. The promise may reject with an error if any of the child
         * elements load functions rejected with an error.
         */
-       load: function() {
-               var section_ids = this.cfgsections(),
-                   tasks = [];
+       load() {
+               const section_ids = this.cfgsections();
+               const tasks = [];
 
                if (Array.isArray(this.children))
-                       for (var i = 0; i < section_ids.length; i++)
+                       for (let i = 0; i < section_ids.length; i++)
                                tasks.push(this.loadChildren(section_ids[i])
                                        .then(Function.prototype.bind.call(function(section_id, set_values) {
-                                               for (var i = 0; i < set_values.length; i++)
+                                               for (let i = 0; i < set_values.length; i++)
                                                        this.children[i].cfgvalue(section_id, set_values[i]);
                                        }, this, section_ids[i])));
 
@@ -928,13 +932,13 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * been parsed. The returned promise is rejected if any parsed values are
         * not meeting the validation constraints of their respective elements.
         */
-       parse: function() {
-               var section_ids = this.cfgsections(),
-                   tasks = [];
+       parse() {
+               const section_ids = this.cfgsections();
+               const tasks = [];
 
                if (Array.isArray(this.children))
-                       for (var i = 0; i < section_ids.length; i++)
-                               for (var j = 0; j < this.children.length; j++)
+                       for (let i = 0; i < section_ids.length; i++)
+                               for (let j = 0; j < this.children.length; j++)
                                        tasks.push(this.children[j].parse(section_ids[i]));
 
                return Promise.all(tasks);
@@ -969,22 +973,22 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * @throws {Error}
         * Throws an exception if a tab with the same `name` already exists.
         */
-       tab: function(name, title, description) {
+       tab(name, title, description) {
                if (this.tabs && this.tabs[name])
                        throw 'Tab already declared';
 
-               var entry = {
-                       name: name,
-                       title: title,
-                       description: description,
+               const entry = {
+                       name,
+                       title,
+                       description,
                        children: []
                };
 
-               this.tabs = this.tabs || [];
+               this.tabs ??= [];
                this.tabs.push(entry);
                this.tabs[name] = entry;
 
-               this.tab_names = this.tab_names || [];
+               this.tab_names ??= [];
                this.tab_names.push(name);
        },
 
@@ -1012,11 +1016,11 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * @returns {LuCI.form.AbstractValue}
         * Returns the instantiated option class instance.
         */
-       option: function(cbiClass /*, ... */) {
+       option(cbiClass, ...args) {
                if (!CBIAbstractValue.isSubclass(cbiClass))
                        throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
 
-               var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
+               const obj = cbiClass.instantiate([ this.map, this, ...args ]);
                this.append(obj);
                return obj;
        },
@@ -1049,13 +1053,14 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * @returns {LuCI.form.AbstractValue}
         * Returns the instantiated option class instance.
         */
-       taboption: function(tabName /*, ... */) {
-               if (!this.tabs || !this.tabs[tabName])
+       taboption(tabName, ...args) {
+               if (!this.tabs?.[tabName])
                        throw L.error('ReferenceError', 'Associated tab not declared');
 
-               var obj = this.option.apply(this, this.varargs(arguments, 1));
+               const obj = this.option(...args);
                obj.tab = tabName;
                this.tabs[tabName].children.push(obj);
+
                return obj;
        },
 
@@ -1080,10 +1085,10 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * configuration values or just a single configuration value, depending
         * on the amount of passed arguments.
         */
-       cfgvalue: function(section_id, option) {
-               var rv = (arguments.length == 1) ? {} : null;
+       cfgvalue(section_id, option) {
+               const rv = (arguments.length == 1) ? {} : null;
 
-               for (var i = 0, o; (o = this.children[i]) != null; i++)
+               for (let i = 0, o; (o = this.children[i]) != null; i++)
                        if (rv)
                                rv[o.option] = o.cfgvalue(section_id);
                        else if (o.option == option)
@@ -1113,11 +1118,11 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * widget input values or just a single widget input value, depending
         * on the amount of passed arguments.
         */
-       formvalue: function(section_id, option) {
-               var rv = (arguments.length == 1) ? {} : null;
+       formvalue(section_id, option) {
+               const rv = (arguments.length == 1) ? {} : null;
 
-               for (var i = 0, o; (o = this.children[i]) != null; i++) {
-                       var func = this.map.root ? this.children[i].formvalue : this.children[i].cfgvalue;
+               for (let i = 0, o; (o = this.children[i]) != null; i++) {
+                       const func = this.map.root ? this.children[i].formvalue : this.children[i].cfgvalue;
 
                        if (rv)
                                rv[o.option] = func.call(o, section_id);
@@ -1149,10 +1154,10 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * widget input values or just a single widget input value, depending
         * on the amount of passed arguments.
         */
-       getUIElement: function(section_id, option) {
-               var rv = (arguments.length == 1) ? {} : null;
+       getUIElement(section_id, option) {
+               const rv = (arguments.length == 1) ? {} : null;
 
-               for (var i = 0, o; (o = this.children[i]) != null; i++)
+               for (let i = 0, o; (o = this.children[i]) != null; i++)
                        if (rv)
                                rv[o.option] = o.getUIElement(section_id);
                        else if (o.option == option)
@@ -1179,10 +1184,10 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * option instance objects or just a single object instance value,
         * depending on the amount of passed arguments.
         */
-       getOption: function(option) {
-               var rv = (arguments.length == 0) ? {} : null;
+       getOption(option) {
+               const rv = (arguments.length == 0) ? {} : null;
 
-               for (var i = 0, o; (o = this.children[i]) != null; i++)
+               for (let i = 0, o; (o = this.children[i]) != null; i++)
                        if (rv)
                                rv[o.option] = o;
                        else if (o.option == option)
@@ -1192,13 +1197,13 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
        },
 
        /** @private */
-       renderUCISection: function(section_id) {
-               var renderTasks = [];
+       renderUCISection(section_id) {
+               const renderTasks = [];
 
                if (!this.tabs)
                        return this.renderOptions(null, section_id);
 
-               for (var i = 0; i < this.tab_names.length; i++)
+               for (let i = 0; i < this.tab_names.length; i++)
                        renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
 
                return Promise.all(renderTasks)
@@ -1206,19 +1211,19 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
        },
 
        /** @private */
-       renderTabContainers: function(section_id, nodes) {
-               var config_name = this.uciconfig || this.map.config,
-                   containerEls = E([]);
-
-               for (var i = 0; i < nodes.length; i++) {
-                       var tab_name = this.tab_names[i],
-                           tab_data = this.tabs[tab_name],
-                           containerEl = E('div', {
-                               'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
-                               'data-tab': tab_name,
-                               'data-tab-title': tab_data.title,
-                               'data-tab-active': tab_name === this.selected_tab
-                           });
+       renderTabContainers(section_id, nodes) {
+               const config_name = this.uciconfig ?? this.map.config;
+               const containerEls = E([]);
+
+               for (let i = 0; i < nodes.length; i++) {
+                       const tab_name = this.tab_names[i];
+                       const tab_data = this.tabs[tab_name];
+                       const containerEl = E('div', {
+                               'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
+                               'data-tab': tab_name,
+                               'data-tab-title': tab_data.title,
+                               'data-tab-active': tab_name === this.selected_tab
+                       });
 
                        if (tab_data.description != null && tab_data.description != '')
                                containerEl.appendChild(
@@ -1232,25 +1237,25 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
        },
 
        /** @private */
-       renderOptions: function(tab_name, section_id) {
-               var in_table = (this instanceof CBITableSection);
+       renderOptions(tab_name, section_id) {
+               const in_table = (this instanceof CBITableSection);
                return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
-                       var optionEls = E([]);
-                       for (var i = 0; i < nodes.length; i++)
+                       const optionEls = E([]);
+                       for (let i = 0; i < nodes.length; i++)
                                optionEls.appendChild(nodes[i]);
                        return optionEls;
                });
        },
 
        /** @private */
-       checkDepends: function(ev, n) {
-               var changed = false,
-                   sids = this.cfgsections();
+       checkDepends(ev, n) {
+               let changed = false;
+               const sids = this.cfgsections();
 
-               for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
-                       for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
-                               var isActive = o.isActive(sid),
-                                   isSatisified = o.checkDepends(sid);
+               for (let i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
+                       for (let j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
+                               let isActive = o.isActive(sid);
+                               const isSatisified = o.checkDepends(sid);
 
                                if (isActive != isSatisified) {
                                        o.setActive(sid, !isActive);
@@ -1268,7 +1273,7 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
 });
 
 
-var isEqual = function(x, y) {
+function isEqual(x, y) {
        if (typeof(y) == 'object' && y instanceof RegExp)
                return (x == null) ? false : y.test(x);
 
@@ -1282,12 +1287,12 @@ var isEqual = function(x, y) {
                if (x.length != y.length)
                        return false;
 
-               for (var i = 0; i < x.length; i++)
+               for (let i = 0; i < x.length; i++)
                        if (!isEqual(x[i], y[i]))
                                return false;
        }
        else if (typeof(x) == 'object') {
-               for (var k in x) {
+               for (const k in x) {
                        if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
                                return false;
 
@@ -1295,7 +1300,7 @@ var isEqual = function(x, y) {
                                return false;
                }
 
-               for (var k in y)
+               for (const k in y)
                        if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
                                return false;
        }
@@ -1306,9 +1311,9 @@ var isEqual = function(x, y) {
        return true;
 };
 
-var isContained = function(x, y) {
+function isContained(x, y) {
        if (Array.isArray(x)) {
-               for (var i = 0; i < x.length; i++)
+               for (let i = 0; i < x.length; i++)
                        if (x[i] == y)
                                return true;
        }
@@ -1337,9 +1342,9 @@ var isContained = function(x, y) {
  *
  * This class is private and not directly accessible by user code.
  */
-var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractValue.prototype */ {
-       __init__: function(map, section, option /*, ... */) {
-               this.super('__init__', this.varargs(arguments, 3));
+const CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractValue.prototype */ {
+       __init__(map, section, option, ...args) {
+               this.super('__init__', args);
 
                this.section = section;
                this.option = option;
@@ -1566,20 +1571,20 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         *
         * <ul>
         *   <li>
-        *    <code>!reverse</code><br>
-        *    Invert the dependency, instead of requiring another option to be
-        *    equal to the dependency value, that option should <em>not</em> be
-        *    equal.
+        *      <code>!reverse</code><br>
+        *      Invert the dependency, instead of requiring another option to be
+        *      equal to the dependency value, that option should <em>not</em> be
+        *      equal.
         *   </li>
         *   <li>
-        *    <code>!contains</code><br>
-        *    Instead of requiring an exact match, the dependency is considered
-        *    satisfied when the dependency value is contained within the option
-        *    value.
+        *      <code>!contains</code><br>
+        *      Instead of requiring an exact match, the dependency is considered
+        *      satisfied when the dependency value is contained within the option
+        *      value.
         *   </li>
         *   <li>
-        *    <code>!default</code><br>
-        *    The dependency is always satisfied
+        *      <code>!default</code><br>
+        *      The dependency is always satisfied
         *   </li>
         * </ul>
         *
@@ -1605,7 +1610,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         *  </li>
         *  <li>
         *   <code>opt.depends({ foo: "test" })<br>
-        *         opt.depends({ bar: "qrx" })</code><br>
+        *               opt.depends({ bar: "qrx" })</code><br>
         *   Require either <code>foo</code> to be set to <code>test</code>,
         *   <em>or</em> the <code>bar</code> option to be <code>qrx</code>.
         *  </li>
@@ -1629,8 +1634,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * specifies the expected value. In case an object is passed as first
         * argument, this parameter is ignored.
         */
-       depends: function(field, value) {
-               var deps;
+       depends(field, value) {
+               let deps;
 
                if (typeof(field) === 'string')
                        deps = {}, deps[field] = value;
@@ -1641,15 +1646,15 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
        },
 
        /** @private */
-       transformDepList: function(section_id, deplist) {
-               var list = deplist || this.deps,
-                   deps = [];
+       transformDepList(section_id, deplist) {
+               const list = deplist ?? this.deps;
+               const deps = [];
 
                if (Array.isArray(list)) {
-                       for (var i = 0; i < list.length; i++) {
-                               var dep = {};
+                       for (let i = 0; i < list.length; i++) {
+                               const dep = {};
 
-                               for (var k in list[i]) {
+                               for (const k in list[i]) {
                                        if (list[i].hasOwnProperty(k)) {
                                                if (k.charAt(0) === '!')
                                                        dep[k] = list[i][k];
@@ -1657,14 +1662,14 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
                                                        dep['cbid.%s'.format(k)] = list[i][k];
                                                else
                                                        dep['cbid.%s.%s.%s'.format(
-                                                               this.uciconfig || this.section.uciconfig || this.map.config,
-                                                               this.ucisection || section_id,
+                                                               this.uciconfig ?? this.section.uciconfig ?? this.map.config,
+                                                               this.ucisection ?? section_id,
                                                                k
                                                        )] = list[i][k];
                                        }
                                }
 
-                               for (var k in dep) {
+                               for (const k in dep) {
                                        if (dep.hasOwnProperty(k)) {
                                                deps.push(dep);
                                                break;
@@ -1677,22 +1682,22 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
        },
 
        /** @private */
-       transformChoices: function() {
+       transformChoices() {
                if (!Array.isArray(this.keylist) || this.keylist.length == 0)
                        return null;
 
-               var choices = {};
+               const choices = {};
 
-               for (var i = 0; i < this.keylist.length; i++)
+               for (let i = 0; i < this.keylist.length; i++)
                        choices[this.keylist[i]] = this.vallist[i];
 
                return choices;
        },
 
        /** @private */
-       checkDepends: function(section_id) {
-               var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
-                   active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
+       checkDepends(section_id) {
+               const config_name = this.uciconfig ?? this.section.uciconfig ?? this.map.config;
+               const active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
 
                if (active)
                        this.updateDefaultValue(section_id);
@@ -1701,15 +1706,16 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
        },
 
        /** @private */
-       updateDefaultValue: function(section_id) {
+       updateDefaultValue(section_id) {
                if (!L.isObject(this.defaults))
                        return;
 
-               var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
-                   cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
-                   default_defval = null, satisified_defval = null;
+               const config_name = this.uciconfig ?? this.section.uciconfig ?? this.map.config;
+               const cfgvalue = L.toArray(this.cfgvalue(section_id))[0];
+               let default_defval = null;
+               let satisified_defval = null;
 
-               for (var value in this.defaults) {
+               for (const value in this.defaults) {
                        if (!this.defaults[value] || this.defaults[value].length == 0) {
                                default_defval = value;
                                continue;
@@ -1723,7 +1729,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
                if (satisified_defval == null)
                        satisified_defval = default_defval;
 
-               var node = this.map.findElement('id', this.cbid(section_id));
+               const node = this.map.findElement('id', this.cbid(section_id));
                if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
                        dom.callClassMethod(node, 'setValue', satisified_defval);
 
@@ -1747,12 +1753,12 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @returns {string}
         * Returns the element ID.
         */
-       cbid: function(section_id) {
+       cbid(section_id) {
                if (section_id == null)
                        L.error('TypeError', 'Section ID required');
 
                return 'cbid.%s.%s.%s'.format(
-                       this.uciconfig || this.section.uciconfig || this.map.config,
+                       this.uciconfig ?? this.section.uciconfig ?? this.map.config,
                        section_id, this.option);
        },
 
@@ -1775,14 +1781,14 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * The return value of this function is filtered through `Promise.resolve()`
         * so it may return promises if overridden by user code.
         */
-       load: function(section_id) {
+       load(section_id) {
                if (section_id == null)
                        L.error('TypeError', 'Section ID required');
 
                return this.map.data.get(
-                       this.uciconfig || this.section.uciconfig || this.map.config,
-                       this.ucisection || section_id,
-                       this.ucioption || this.option);
+                       this.uciconfig ?? this.section.uciconfig ?? this.map.config,
+                       this.ucisection ?? section_id,
+                       this.ucioption ?? this.option);
        },
 
        /**
@@ -1798,9 +1804,9 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * Returns the `LuCI.ui` element instance or `null` in case the form
         * option implementation does not use `LuCI.ui` widgets.
         */
-       getUIElement: function(section_id) {
-               var node = this.map.findElement('id', this.cbid(section_id)),
-                   inst = node ? dom.findClassInstance(node) : null;
+       getUIElement(section_id) {
+               const node = this.map.findElement('id', this.cbid(section_id));
+               const inst = node ? dom.findClassInstance(node) : null;
                return (inst instanceof ui.AbstractElement) ? inst : null;
        },
 
@@ -1821,16 +1827,16 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @returns {*}
         * Returns the configuration value.
         */
-       cfgvalue: function(section_id, set_value) {
+       cfgvalue(section_id, set_value) {
                if (section_id == null)
                        L.error('TypeError', 'Section ID required');
 
                if (arguments.length == 2) {
-                       this.data = this.data || {};
+                       this.data ??= {};
                        this.data[section_id] = set_value;
                }
 
-               return this.data ? this.data[section_id] : null;
+               return this.data?.[section_id];
        },
 
        /**
@@ -1849,8 +1855,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @returns {*}
         * Returns the current input value.
         */
-       formvalue: function(section_id) {
-               var elem = this.getUIElement(section_id);
+       formvalue(section_id) {
+               const elem = this.getUIElement(section_id);
                return elem ? elem.getValue() : null;
        },
 
@@ -1873,8 +1879,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @returns {string}
         * Returns the text representation of the current input value.
         */
-       textvalue: function(section_id) {
-               var cval = this.cfgvalue(section_id);
+       textvalue(section_id) {
+               let cval = this.cfgvalue(section_id);
 
                if (cval == null)
                        cval = this.default;
@@ -1908,7 +1914,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * return value is treated as failure, converted to a string and displayed
         * as error message to the user.
         */
-       validate: function(section_id, value) {
+       validate(section_id, value) {
                return true;
        },
 
@@ -1922,8 +1928,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * Returns `true` if the input value currently is valid, otherwise it
         * returns `false`.
         */
-       isValid: function(section_id) {
-               var elem = this.getUIElement(section_id);
+       isValid(section_id) {
+               const elem = this.getUIElement(section_id);
                return elem ? elem.isValid() : true;
        },
 
@@ -1936,8 +1942,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @returns {string}
         * The validation error at this time
         */
-       getValidationError: function (section_id) {
-               var elem = this.getUIElement(section_id);
+       getValidationError(section_id) {
+               const elem = this.getUIElement(section_id);
                return elem ? elem.getValidationError() : '';
        },
 
@@ -1954,14 +1960,14 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * Returns `true` if the option element currently is active, otherwise it
         * returns `false`.
         */
-       isActive: function(section_id) {
-               var field = this.map.findElement('data-field', this.cbid(section_id));
+       isActive(section_id) {
+               const field = this.map.findElement('data-field', this.cbid(section_id));
                return (field != null && !field.classList.contains('hidden'));
        },
 
        /** @private */
-       setActive: function(section_id, active) {
-               var field = this.map.findElement('data-field', this.cbid(section_id));
+       setActive(section_id, active) {
+               const field = this.map.findElement('data-field', this.cbid(section_id));
 
                if (field && field.classList.contains('hidden') == active) {
                        field.classList[active ? 'remove' : 'add']('hidden');
@@ -1976,8 +1982,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
        },
 
        /** @private */
-       triggerValidation: function(section_id) {
-               var elem = this.getUIElement(section_id);
+       triggerValidation(section_id) {
+               const elem = this.getUIElement(section_id);
                return elem ? elem.triggerValidation() : true;
        },
 
@@ -1995,27 +2001,27 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * validated or rejecting in case the input value does not meet the
         * validation constraints.
         */
-       parse: function(section_id) {
-               var active = this.isActive(section_id);
+       parse(section_id) {
+               const active = this.isActive(section_id);
 
                if (active && !this.isValid(section_id)) {
-                       var title = this.stripTags(this.title).trim(),
-                           error = this.getValidationError(section_id);
+                       const title = this.stripTags(this.title).trim();
+                       const error = this.getValidationError(section_id);
 
                        return Promise.reject(new TypeError(
-                               _('Option "%s" contains an invalid input value.').format(title || this.option) + ' ' + error));
+                               `${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${error}`));
                }
 
                if (active) {
-                       var cval = this.cfgvalue(section_id),
-                           fval = this.formvalue(section_id);
+                       const cval = this.cfgvalue(section_id);
+                       const fval = this.formvalue(section_id);
 
                        if (fval == null || fval == '') {
                                if (this.rmempty || this.optional) {
                                        return Promise.resolve(this.remove(section_id));
                                }
                                else {
-                                       var title = this.stripTags(this.title).trim();
+                                       const title = this.stripTags(this.title).trim();
 
                                        return Promise.reject(new TypeError(
                                                _('Option "%s" must not be empty.').format(title || this.option)));
@@ -2052,11 +2058,11 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @param {string|string[]}     formvalue
         * The input value to write.
         */
-       write: function(section_id, formvalue) {
+       write(section_id, formvalue) {
                return this.map.data.set(
-                       this.uciconfig || this.section.uciconfig || this.map.config,
-                       this.ucisection || section_id,
-                       this.ucioption || this.option,
+                       this.uciconfig ?? this.section.uciconfig ?? this.map.config,
+                       this.ucisection ?? section_id,
+                       this.ucioption ?? this.option,
                        formvalue);
        },
 
@@ -2075,20 +2081,20 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
         * @param {string} section_id
         * The configuration section ID
         */
-       remove: function(section_id) {
-               var this_cfg = this.uciconfig || this.section.uciconfig || this.map.config,
-                   this_sid = this.ucisection || section_id,
-                   this_opt = this.ucioption || this.option;
+       remove(section_id) {
+               const this_cfg = this.uciconfig ?? this.section.uciconfig ?? this.map.config;
+               const this_sid = this.ucisection ?? section_id;
+               const this_opt = this.ucioption ?? this.option;
 
-               for (var i = 0; i < this.section.children.length; i++) {
-                       var sibling = this.section.children[i];
+               for (let i = 0; i < this.section.children.length; i++) {
+                       const sibling = this.section.children[i];
 
                        if (sibling === this || sibling.ucioption == null)
                                continue;
 
-                       var sibling_cfg = sibling.uciconfig || sibling.section.uciconfig || sibling.map.config,
-                           sibling_sid = sibling.ucisection || section_id,
-                           sibling_opt = sibling.ucioption || sibling.option;
+                       const sibling_cfg = sibling.uciconfig ?? sibling.section.uciconfig ?? sibling.map.config;
+                       const sibling_sid = sibling.ucisection ?? section_id;
+                       const sibling_opt = sibling.ucioption ?? sibling.option;
 
                        if (this_cfg != sibling_cfg || this_sid != sibling_sid || this_opt != sibling_opt)
                                continue;
@@ -2134,7 +2140,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
  * @param {string} [description]
  * The description text of the form section element.
  */
-var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSection.prototype */ {
+const CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSection.prototype */ {
        __name__: 'CBI.TypedSection',
 
        /**
@@ -2193,36 +2199,36 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
         */
 
        /** @override */
-       cfgsections: function() {
-               return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
+       cfgsections() {
+               return this.map.data.sections(this.uciconfig ?? this.map.config, this.sectiontype)
                        .map(function(s) { return s['.name'] })
                        .filter(L.bind(this.filter, this));
        },
 
        /** @private */
-       handleAdd: function(ev, name) {
-               var config_name = this.uciconfig || this.map.config;
+       handleAdd(ev, name) {
+               const config_name = this.uciconfig ?? this.map.config;
 
                this.map.data.add(config_name, this.sectiontype, name);
                return this.map.save(null, true);
        },
 
        /** @private */
-       handleRemove: function(section_id, ev) {
-               var config_name = this.uciconfig || this.map.config;
+       handleRemove(section_id, ev) {
+               const config_name = this.uciconfig ?? this.map.config;
 
                this.map.data.remove(config_name, section_id);
                return this.map.save(null, true);
        },
 
        /** @private */
-       renderSectionAdd: function(extra_class) {
+       renderSectionAdd(extra_class) {
                if (!this.addremove)
                        return E([]);
 
-               var createEl = E('div', { 'class': 'cbi-section-create' }),
-                   config_name = this.uciconfig || this.map.config,
-                   btn_title = this.titleFn('addbtntitle');
+               const createEl = E('div', { 'class': 'cbi-section-create' });
+               const config_name = this.uciconfig ?? this.map.config;
+               const btn_title = this.titleFn('addbtntitle');
 
                if (extra_class != null)
                        createEl.classList.add(extra_class);
@@ -2230,13 +2236,13 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
                if (this.anonymous) {
                        createEl.appendChild(E('button', {
                                'class': 'cbi-button cbi-button-add',
-                               'title': btn_title || _('Add'),
+                               'title': btn_title ?? _('Add'),
                                'click': ui.createHandlerFn(this, 'handleAdd'),
                                'disabled': this.map.readonly || null
-                       }, [ btn_title || _('Add') ]));
+                       }, [ btn_title ?? _('Add') ]));
                }
                else {
-                       var nameEl = E('input', {
+                       const nameEl = E('input', {
                                'type': 'text',
                                'class': 'cbi-section-create-name',
                                'disabled': this.map.readonly || null
@@ -2246,7 +2252,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
                                E('div', {}, nameEl),
                                E('button', {
                                        'class': 'cbi-button cbi-button-add',
-                                       'title': btn_title || _('Add'),
+                                       'title': btn_title ?? _('Add'),
                                        'click': ui.createHandlerFn(this, function(ev) {
                                                if (nameEl.classList.contains('cbi-input-invalid'))
                                                        return;
@@ -2254,12 +2260,12 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
                                                return this.handleAdd(ev, nameEl.value);
                                        }),
                                        'disabled': this.map.readonly || true
-                               }, [ btn_title || _('Add') ])
+                               }, [ btn_title ?? _('Add') ])
                        ]);
 
                        if (this.map.readonly !== true) {
                                ui.addValidator(nameEl, 'uciname', true, function(v) {
-                                       var button = createEl.querySelector('.cbi-section-create > .cbi-button-add');
+                                       const button = createEl.querySelector('.cbi-section-create > .cbi-button-add');
                                        if (v !== '') {
                                                button.disabled = null;
                                                return true;
@@ -2276,20 +2282,21 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
        },
 
        /** @private */
-       renderSectionPlaceholder: function() {
+       renderSectionPlaceholder() {
                return E('em', _('This section contains no values yet'));
        },
 
        /** @private */
-       renderContents: function(cfgsections, nodes) {
-               var section_id = null,
-                   config_name = this.uciconfig || this.map.config,
-                   sectionEl = E('div', {
-                               'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
-                               'class': 'cbi-section',
-                               'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
-                               'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
-                       });
+       renderContents(cfgsections, nodes) {
+               const section_id = null;
+               const config_name = this.uciconfig ?? this.map.config;
+
+               const sectionEl = E('div', {
+                       'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+                       'class': 'cbi-section',
+                       'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
+                       'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
+               });
 
                if (this.title != null && this.title != '')
                        sectionEl.appendChild(E('h3', {}, this.title));
@@ -2297,7 +2304,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
                if (this.description != null && this.description != '')
                        sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
 
-               for (var i = 0; i < nodes.length; i++) {
+               for (let i = 0; i < nodes.length; i++) {
                        if (this.addremove) {
                                sectionEl.appendChild(
                                        E('div', { 'class': 'cbi-section-remove right' },
@@ -2332,11 +2339,11 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
        },
 
        /** @override */
-       render: function() {
-               var cfgsections = this.cfgsections(),
-                   renderTasks = [];
+       render() {
+               const cfgsections = this.cfgsections();
+               const renderTasks = [];
 
-               for (var i = 0; i < cfgsections.length; i++)
+               for (let i = 0; i < cfgsections.length; i++)
                        renderTasks.push(this.renderUCISection(cfgsections[i]));
 
                return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
@@ -2372,7 +2379,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
  * @param {string} [description]
  * The description text of the form section element.
  */
-var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.prototype */ {
+const CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.prototype */ {
        __name__: 'CBI.TableSection',
 
        /**
@@ -2517,27 +2524,29 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
         * @override
         * @throws Throws an exception when invoked.
         */
-       tab: function() {
+       tab() {
                throw 'Tabs are not supported by TableSection';
        },
 
        /** @private */
-       renderContents: function(cfgsections, nodes) {
-               var section_id = null,
-                   config_name = this.uciconfig || this.map.config,
-                   max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
-                   has_more = max_cols < this.children.length,
-                   drag_sort = this.sortable && !('ontouchstart' in window),
-                   touch_sort = this.sortable && ('ontouchstart' in window),
-                   sectionEl = E('div', {
-                               'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
-                               'class': 'cbi-section cbi-tblsection',
-                               'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
-                               'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
-                       }),
-                       tableEl = E('table', {
-                               'class': 'table cbi-section-table'
-                       });
+       renderContents(cfgsections, nodes) {
+               const section_id = null;
+               const config_name = this.uciconfig ?? this.map.config;
+               const max_cols = this.max_cols ?? this.children.length;
+               const has_more = max_cols < this.children.length;
+               const drag_sort = this.sortable && !('ontouchstart' in window);
+               const touch_sort = this.sortable && ('ontouchstart' in window);
+
+               const sectionEl = E('div', {
+                       'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+                       'class': 'cbi-section cbi-tblsection',
+                       'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
+                       'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
+               });
+
+               const tableEl = E('table', {
+                       'class': 'table cbi-section-table'
+               });
 
                if (this.title != null && this.title != '')
                        sectionEl.appendChild(E('h3', {}, this.title));
@@ -2545,15 +2554,15 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                if (this.description != null && this.description != '')
                        sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
 
-               tableEl.appendChild(this.renderHeaderRows(max_cols));
+               tableEl.appendChild(this.renderHeaderRows(false));
 
-               for (var i = 0; i < nodes.length; i++) {
-                       var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
+               for (let i = 0; i < nodes.length; i++) {
+                       let sectionname = this.titleFn('sectiontitle', cfgsections[i]);
 
                        if (sectionname == null)
                                sectionname = cfgsections[i];
 
-                       var trEl = E('tr', {
+                       const trEl = E('tr', {
                                'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
                                'class': 'tr cbi-section-table-row',
                                'data-sid': cfgsections[i],
@@ -2575,7 +2584,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                trEl.classList.add(!(tableEl.childNodes.length % 2)
                                        ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
 
-                       for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
+                       for (let j = 0; j < max_cols && nodes[i].firstChild; j++)
                                trEl.appendChild(nodes[i].firstChild);
 
                        trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
@@ -2596,15 +2605,15 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       renderHeaderRows: function(max_cols, has_action) {
-               var has_titles = false,
-                   has_descriptions = false,
-                   max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
-                   has_more = max_cols < this.children.length,
-                   anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
-                   trEls = E([]);
-
-               for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+       renderHeaderRows(has_action) {
+               let has_titles = false;
+               let has_descriptions = false;
+               const max_cols = this.max_cols ?? this.children.length;
+               const has_more = max_cols < this.children.length;
+               const anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous';
+               const trEls = E([]);
+
+               for (let i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
                        if (opt.modalonly)
                                continue;
 
@@ -2613,13 +2622,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                }
 
                if (has_titles) {
-                       var trEl = E('tr', {
-                               'class': 'tr cbi-section-table-titles ' + anon_class,
+                       const trEl = E('tr', {
+                               'class': `tr cbi-section-table-titles ${anon_class}`,
                                'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null,
                                'click': this.sortable ? ui.createHandlerFn(this, 'handleSort') : null
                        });
 
-                       for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+                       for (let i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
                                if (opt.modalonly)
                                        continue;
 
@@ -2631,13 +2640,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
 
                                if (opt.width != null)
                                        trEl.lastElementChild.style.width =
-                                               (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+                                               (typeof(opt.width) == 'number') ? `${opt.width}px` : opt.width;
 
                                if (opt.titleref)
                                        trEl.lastElementChild.appendChild(E('a', {
                                                'href': opt.titleref,
                                                'class': 'cbi-title-ref',
-                                               'title': this.titledesc || _('Go to relevant configuration page')
+                                               'title': this.titledesc ?? _('Go to relevant configuration page')
                                        }, opt.title));
                                else
                                        dom.content(trEl.lastElementChild, opt.title);
@@ -2652,11 +2661,11 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                }
 
                if (has_descriptions && !this.nodescriptions) {
-                       var trEl = E('tr', {
-                               'class': 'tr cbi-section-table-descr ' + anon_class
+                       const trEl = E('tr', {
+                               'class': `tr cbi-section-table-descr ${anon_class}`
                        });
 
-                       for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+                       for (let i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
                                if (opt.modalonly)
                                        continue;
 
@@ -2667,7 +2676,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
 
                                if (opt.width != null)
                                        trEl.lastElementChild.style.width =
-                                               (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+                                               (typeof(opt.width) == 'number') ? `${opt.width}px` : opt.width;
                        }
 
                        if (this.sortable || this.extedit || this.addremove || has_more || has_action)
@@ -2682,13 +2691,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       renderRowActions: function(section_id, more_label) {
-               var config_name = this.uciconfig || this.map.config;
+       renderRowActions(section_id, more_label) {
+               const config_name = this.uciconfig ?? this.map.config;
 
                if (!this.sortable && !this.extedit && !this.addremove && !more_label)
                        return E([]);
 
-               var tdEl = E('td', {
+               const tdEl = E('td', {
                        'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
                }, E('div'));
 
@@ -2704,7 +2713,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                }
 
                if (this.extedit) {
-                       var evFn = null;
+                       let evFn = null;
 
                        if (typeof(this.extedit) == 'function')
                                evFn = L.bind(this.extedit, this);
@@ -2733,15 +2742,15 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                }
 
                if (this.addremove) {
-                       var btn_title = this.titleFn('removebtntitle', section_id);
+                       const btn_title = this.titleFn('removebtntitle', section_id);
 
                        dom.append(tdEl.lastElementChild,
                                E('button', {
-                                       'title': btn_title || _('Delete'),
+                                       'title': btn_title ?? _('Delete'),
                                        'class': 'cbi-button cbi-button-remove',
                                        'click': ui.createHandlerFn(this, 'handleRemove', section_id),
                                        'disabled': this.map.readonly || null
-                               }, [ btn_title || _('Delete') ])
+                               }, [ btn_title ?? _('Delete') ])
                        );
                }
 
@@ -2749,13 +2758,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleDragInit: function(ev) {
+       handleDragInit(ev) {
                scope.dragState = { node: ev.target };
        },
 
        /** @private */
-       handleDragStart: function(ev) {
-               if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
+       handleDragStart(ev) {
+               if (!scope.dragState?.node.classList.contains('drag-handle')) {
                        scope.dragState = null;
                        ev.preventDefault();
                        return false;
@@ -2767,10 +2776,10 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleDragOver: function(ev) {
-               var n = scope.dragState.targetNode,
-                   r = scope.dragState.rect,
-                   t = r.top + r.height / 2;
+       handleDragOver(ev) {
+               const n = scope.dragState.targetNode;
+               const r = scope.dragState.rect;
+               const t = r.top + r.height / 2;
 
                if (ev.clientY <= t) {
                        n.classList.remove('drag-over-below');
@@ -2787,20 +2796,20 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleDragEnter: function(ev) {
+       handleDragEnter(ev) {
                scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
                scope.dragState.targetNode = ev.currentTarget;
        },
 
        /** @private */
-       handleDragLeave: function(ev) {
+       handleDragLeave(ev) {
                ev.currentTarget.classList.remove('drag-over-above');
                ev.currentTarget.classList.remove('drag-over-below');
        },
 
        /** @private */
-       handleDragEnd: function(ev) {
-               var n = ev.target;
+       handleDragEnd(ev) {
+               const n = ev.target;
 
                n.style.opacity = '';
                n.classList.add('flash');
@@ -2812,24 +2821,24 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleDrop: function(ev) {
-               var s = scope.dragState;
+       handleDrop(ev) {
+               const s = scope.dragState;
 
                if (s.node && s.targetNode) {
-                       var config_name = this.uciconfig || this.map.config,
-                           ref_node = s.targetNode,
-                           after = false;
+                       const config_name = this.uciconfig ?? this.map.config;
+                       let ref_node = s.targetNode;
+                       let after = false;
 
-                   if (ref_node.classList.contains('drag-over-below')) {
-                       ref_node = ref_node.nextElementSibling;
-                       after = true;
-                   }
+                       if (ref_node.classList.contains('drag-over-below')) {
+                               ref_node = ref_node.nextElementSibling;
+                               after = true;
+                       }
 
-                   var sid1 = s.node.getAttribute('data-sid'),
-                       sid2 = s.targetNode.getAttribute('data-sid');
+                       const sid1 = s.node.getAttribute('data-sid');
+                       const sid2 = s.targetNode.getAttribute('data-sid');
 
-                   s.node.parentNode.insertBefore(s.node, ref_node);
-                   this.map.data.move(config_name, sid1, sid2, after);
+                       s.node.parentNode.insertBefore(s.node, ref_node);
+                       this.map.data.move(config_name, sid1, sid2, after);
                }
 
                scope.dragState = null;
@@ -2840,12 +2849,14 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       determineBackgroundColor: function(node) {
-               var r = 255, g = 255, b = 255;
+       determineBackgroundColor(node) {
+               let r = 255;
+               let g = 255;
+               let b = 255;
 
                while (node) {
-                       var s = window.getComputedStyle(node),
-                           c = (s.getPropertyValue('background-color') || '').replace(/ /g, '');
+                       const s = window.getComputedStyle(node);
+                       const c = (s.getPropertyValue('background-color') ?? '').replace(/ /g, '');
 
                        if (c != '' && c != 'transparent' && c != 'rgba(0,0,0,0)') {
                                if (/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.test(c)) {
@@ -2869,24 +2880,24 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleTouchMove: function(ev) {
+       handleTouchMove(ev) {
                if (!ev.target.classList.contains('drag-handle'))
                        return;
 
-               var touchLoc = ev.targetTouches[0],
-                   rowBtn = ev.target,
-                   rowElem = dom.parent(rowBtn, '.tr'),
-                   htmlElem = document.querySelector('html'),
-                   dragHandle = document.querySelector('.touchsort-element'),
-                   viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+               const touchLoc = ev.targetTouches[0];
+               const rowBtn = ev.target;
+               const rowElem = dom.parent(rowBtn, '.tr');
+               const htmlElem = document.querySelector('html');
+               let dragHandle = document.querySelector('.touchsort-element');
+               const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight ?? 0);
 
                if (!dragHandle) {
-                       var rowRect = rowElem.getBoundingClientRect(),
-                           btnRect = rowBtn.getBoundingClientRect(),
-                           paddingLeft = btnRect.left - rowRect.left,
-                           paddingRight = rowRect.right - btnRect.right,
-                           colorBg = this.determineBackgroundColor(rowElem),
-                           colorFg = (colorBg[0] * 0.299 + colorBg[1] * 0.587 + colorBg[2] * 0.114) > 186 ? [ 0, 0, 0 ] : [ 255, 255, 255 ];
+                       const rowRect = rowElem.getBoundingClientRect();
+                       const btnRect = rowBtn.getBoundingClientRect();
+                       const paddingLeft = btnRect.left - rowRect.left;
+                       const paddingRight = rowRect.right - btnRect.right;
+                       const colorBg = this.determineBackgroundColor(rowElem);
+                       const colorFg = (colorBg[0] * 0.299 + colorBg[1] * 0.587 + colorBg[2] * 0.114) > 186 ? [ 0, 0, 0 ] : [ 255, 255, 255 ];
 
                        dragHandle = E('div', { 'class': 'touchsort-element' }, [
                                E('strong', [ rowElem.getAttribute('data-title') ]),
@@ -2897,10 +2908,10 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                position: 'absolute',
                                boxShadow: '0 0 3px rgba(%d, %d, %d, 1)'.format(colorFg[0], colorFg[1], colorFg[2]),
                                background: 'rgba(%d, %d, %d, 0.8)'.format(colorBg[0], colorBg[1], colorBg[2]),
-                               top: rowRect.top + 'px',
-                               left: rowRect.left + 'px',
-                               width: rowRect.width + 'px',
-                               height: (rowBtn.offsetHeight + 4) + 'px'
+                               top: `${rowRect.top}px`,
+                               left: `${rowRect.left}px`,
+                               width: `${rowRect.width}px`,
+                               height: `${rowBtn.offsetHeight + 4}px`
                        });
 
                        Object.assign(dragHandle.firstElementChild.style, {
@@ -2911,14 +2922,14 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                textOverflow: 'ellipsis',
                                left: (paddingRight > paddingLeft) ? '' : '5px',
                                right: (paddingRight > paddingLeft) ? '5px' : '',
-                               width: (Math.max(paddingLeft, paddingRight) - 10) + 'px'
+                               width: `${Math.max(paddingLeft, paddingRight) - 10}px`
                        });
 
                        Object.assign(dragHandle.lastElementChild.style, {
                                position: 'absolute',
                                top: '2px',
-                               left: paddingLeft + 'px',
-                               width: rowBtn.offsetWidth + 'px'
+                               left: `${paddingLeft}px`,
+                               width: `${rowBtn.offsetWidth}px`
                        });
 
                        document.body.appendChild(dragHandle);
@@ -2927,13 +2938,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                        rowBtn.blur();
                }
 
-               dragHandle.style.top = (touchLoc.pageY - (parseInt(dragHandle.style.height) / 2)) + 'px';
+               dragHandle.style.top = `${touchLoc.pageY - (parseInt(dragHandle.style.height) / 2)}px`;
 
                rowElem.parentNode.querySelectorAll('[draggable]').forEach(function(tr, i, trs) {
-                       var trRect = tr.getBoundingClientRect(),
-                           yTop = trRect.top + window.scrollY,
-                           yBottom = trRect.bottom + window.scrollY,
-                           yMiddle = yTop + ((yBottom - yTop) / 2);
+                       const trRect = tr.getBoundingClientRect();
+                       const yTop = trRect.top + window.scrollY;
+                       const yBottom = trRect.bottom + window.scrollY;
+                       const yMiddle = yTop + ((yBottom - yTop) / 2);
 
                        tr.classList.remove('drag-over-above', 'drag-over-below');
 
@@ -2955,29 +2966,29 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleTouchEnd: function(ev) {
-               var rowElem = dom.parent(ev.target, '.tr'),
-                   htmlElem = document.querySelector('html'),
-                   dragHandle = document.querySelector('.touchsort-element'),
-                   targetElem = rowElem.parentNode.querySelector('.drag-over-above, .drag-over-below'),
-                   viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+       handleTouchEnd(ev) {
+               const rowElem = dom.parent(ev.target, '.tr');
+               const htmlElem = document.querySelector('html');
+               const dragHandle = document.querySelector('.touchsort-element');
+               const targetElem = rowElem.parentNode.querySelector('.drag-over-above, .drag-over-below');
+               const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight ?? 0);
 
                if (!dragHandle)
                        return;
 
                if (targetElem) {
-                   var isBelow = targetElem.classList.contains('drag-over-below');
+                       const isBelow = targetElem.classList.contains('drag-over-below');
 
                        rowElem.parentNode.insertBefore(rowElem, isBelow ? targetElem.nextElementSibling : targetElem);
 
                        this.map.data.move(
-                               this.uciconfig || this.map.config,
+                               this.uciconfig ?? this.map.config,
                                rowElem.getAttribute('data-sid'),
                                targetElem.getAttribute('data-sid'),
                                isBelow);
 
                        window.requestAnimationFrame(function() {
-                               var rowRect = rowElem.getBoundingClientRect();
+                               const rowRect = rowElem.getBoundingClientRect();
 
                                if (rowRect.top < 50)
                                        htmlElem.scrollTop = (htmlElem.scrollTop + rowRect.top - 50);
@@ -2994,13 +3005,13 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleModalCancel: function(modalMap, ev) {
-               var prevNode = this.getPreviousModalMap(),
-                   resetTasks = Promise.resolve();
+       handleModalCancel(modalMap, ev) {
+               const prevNode = this.getPreviousModalMap();
+               let resetTasks = Promise.resolve();
 
                if (prevNode) {
-                       var heading = prevNode.parentNode.querySelector('h4'),
-                           prevMap = dom.findClassInstance(prevNode);
+                       const heading = prevNode.parentNode.querySelector('h4');
+                       let prevMap = dom.findClassInstance(prevNode);
 
                        while (prevMap) {
                                resetTasks = resetTasks
@@ -3029,10 +3040,10 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleModalSave: function(modalMap, ev) {
-               var mapNode = this.getActiveModalMap(),
-                   activeMap = dom.findClassInstance(mapNode),
-                   saveTasks = activeMap.save(null, true);
+       handleModalSave(modalMap, ev) {
+               const mapNode = this.getActiveModalMap();
+               let activeMap = dom.findClassInstance(mapNode);
+               let saveTasks = activeMap.save(null, true);
 
                while (activeMap.parent) {
                        activeMap = activeMap.parent;
@@ -3047,15 +3058,15 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       handleSort: function(ev) {
+       handleSort(ev) {
                if (!ev.target.matches('th[data-sortable-row]'))
                        return;
 
-               var th = ev.target,
-                   descending = (th.getAttribute('data-sort-direction') == 'desc'),
-                   config_name = this.uciconfig || this.map.config,
-                   index = 0,
-                   list = [];
+               const th = ev.target;
+               const descending = (th.getAttribute('data-sort-direction') == 'desc');
+               const config_name = this.uciconfig ?? this.map.config;
+               let index = 0;
+               const list = [];
 
                ev.currentTarget.querySelectorAll('th').forEach(function(other_th, i) {
                        if (other_th !== th)
@@ -3065,9 +3076,9 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                });
 
                ev.currentTarget.parentNode.querySelectorAll('tr.cbi-section-table-row').forEach(L.bind(function(tr, i) {
-                       var sid = tr.getAttribute('data-sid'),
-                           opt = tr.childNodes[index].getAttribute('data-name'),
-                           val = this.cfgvalue(sid, opt);
+                       const sid = tr.getAttribute('data-sid');
+                       const opt = tr.childNodes[index].getAttribute('data-name');
+                       const val = this.cfgvalue(sid, opt);
 
                        tr.querySelectorAll('.flash').forEach(function(n) {
                                n.classList.remove('flash')
@@ -3086,9 +3097,10 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                });
 
                window.requestAnimationFrame(L.bind(function() {
-                       var ref_sid, cur_sid;
+                       let ref_sid;
+                       let cur_sid;
 
-                       for (var i = 0; i < list.length; i++) {
+                       for (let i = 0; i < list.length; i++) {
                                list[i][1].childNodes[index].classList.add('flash');
                                th.parentNode.parentNode.appendChild(list[i][1]);
 
@@ -3129,37 +3141,37 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
         * custom logic to perform asynchronous work before the modal dialog
         * is shown.
         */
-       addModalOptions: function(modalSection, section_id, ev) {
+       addModalOptions(modalSection, section_id, ev) {
 
        },
 
        /** @private */
-       getActiveModalMap: function() {
+       getActiveModalMap() {
                return document.querySelector('body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map:not(.hidden)');
        },
 
        /** @private */
-       getPreviousModalMap: function() {
-               var mapNode = this.getActiveModalMap(),
-                   prevNode = mapNode ? mapNode.previousElementSibling : null;
+       getPreviousModalMap() {
+               const mapNode = this.getActiveModalMap();
+               const prevNode = mapNode ? mapNode.previousElementSibling : null;
 
                return (prevNode && prevNode.matches('.cbi-map.hidden')) ? prevNode : null;
        },
 
        /** @private */
-       cloneOptions: function(src_section, dest_section) {
-               for (var i = 0; i < src_section.children.length; i++) {
-                       var o1 = src_section.children[i];
+       cloneOptions(src_section, dest_section) {
+               for (let i = 0; i < src_section.children.length; i++) {
+                       const o1 = src_section.children[i];
 
                        if (o1.modalonly === false && src_section === this)
                                continue;
 
-                       var o2;
+                       let o2;
 
                        if (o1.subsection) {
                                o2 = dest_section.option(o1.constructor, o1.option, o1.subsection.constructor, o1.subsection.sectiontype, o1.subsection.title, o1.subsection.description);
 
-                               for (var k in o1.subsection) {
+                               for (const k in o1.subsection) {
                                        if (!o1.subsection.hasOwnProperty(k))
                                                continue;
 
@@ -3180,7 +3192,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                o2 = dest_section.option(o1.constructor, o1.option, o1.title, o1.description);
                        }
 
-                       for (var k in o1) {
+                       for (const k in o1) {
                                if (!o1.hasOwnProperty(k))
                                        continue;
 
@@ -3201,17 +3213,17 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        },
 
        /** @private */
-       renderMoreOptionsModal: function(section_id, ev) {
-               var parent = this.map,
-                   sref = parent.data.get(parent.config, section_id),
-                   mapNode = this.getActiveModalMap(),
-                   activeMap = mapNode ? dom.findClassInstance(mapNode) : null,
-                   stackedMap = activeMap && (activeMap.parent !== parent || activeMap.section !== section_id);
+       renderMoreOptionsModal(section_id, ev) {
+               const parent = this.map;
+               const sref = parent.data.get(parent.config, section_id);
+               const mapNode = this.getActiveModalMap();
+               const activeMap = mapNode ? dom.findClassInstance(mapNode) : null;
+               const stackedMap = activeMap && (activeMap.parent !== parent || activeMap.section !== section_id);
 
                return (stackedMap ? activeMap.save(null, true) : Promise.resolve()).then(L.bind(function() {
                        section_id = sref['.name'];
 
-                       var m;
+                       let m;
 
                        if (parent instanceof CBIJSONMap) {
                                m = new CBIJSONMap(null, null, null);
@@ -3221,7 +3233,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                m = new CBIMap(parent.config, null, null);
                        }
 
-                       var s = m.section(CBINamedSection, section_id, this.sectiontype);
+                       const s = m.section(CBINamedSection, section_id, this.sectiontype);
 
                        m.parent = parent;
                        m.section = section_id;
@@ -3235,8 +3247,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                        return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(function() {
                                return m.render();
                        }).then(L.bind(function(nodes) {
-                               var title = parent.title,
-                                   name = null;
+                               let title = parent.title;
+                               let name = null;
 
                                if ((name = this.titleFn('modaltitle', section_id)) != null)
                                        title = name;
@@ -3248,7 +3260,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                if (stackedMap) {
                                        mapNode.parentNode
                                                .querySelector('h4')
-                                               .appendChild(E('span', title ? ' » ' + title : ''));
+                                               .appendChild(E('span', title ? ` » ${title}` : ''));
 
                                        mapNode.parentNode
                                                .querySelector('div.right > button')
@@ -3320,7 +3332,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
  * @param {string} [description]
  * The description text of the form section element.
  */
-var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.prototype */ {
+const CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.prototype */ {
        /**
         * Add an option tab to the section.
         *
@@ -3349,16 +3361,16 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
         * @throws {Error}
         * Throws an exception if a tab with the same `name` already exists.
         */
-       tab: function(name, title, description) {
+       tab(name, title, description) {
                CBIAbstractSection.prototype.tab.call(this, name, title, description);
        },
 
        /** @private */
-       handleAdd: function(ev, name) {
-               var config_name = this.uciconfig || this.map.config,
-                   section_id = this.map.data.add(config_name, this.sectiontype, name),
-                   mapNode = this.getPreviousModalMap(),
-                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
+       handleAdd(ev, name) {
+               const config_name = this.uciconfig ?? this.map.config;
+               const section_id = this.map.data.add(config_name, this.sectiontype, name);
+               const mapNode = this.getPreviousModalMap();
+               const prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
 
                prevMap.addedSection = section_id;
 
@@ -3366,18 +3378,18 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
        },
 
        /** @private */
-       handleModalSave: function(/* ... */) {
-               var mapNode = this.getPreviousModalMap(),
-                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
+       handleModalSave(...args) /* ... */{
+               const mapNode = this.getPreviousModalMap();
+               const prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
 
-               return this.super('handleModalSave', arguments);
+               return this.super('handleModalSave', args);
        },
 
        /** @private */
-       handleModalCancel: function(modalMap, ev, isSaving) {
-               var config_name = this.uciconfig || this.map.config,
-                   mapNode = this.getPreviousModalMap(),
-                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
+       handleModalCancel(modalMap, ev, isSaving) {
+               const config_name = this.uciconfig ?? this.map.config;
+               const mapNode = this.getPreviousModalMap();
+               const prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
 
                if (prevMap.addedSection != null && !isSaving)
                        this.map.data.remove(config_name, prevMap.addedSection);
@@ -3388,15 +3400,16 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
        },
 
        /** @private */
-       renderUCISection: function(section_id) {
+       renderUCISection(section_id) {
                return this.renderOptions(null, section_id);
        },
 
        /** @private */
-       renderChildren: function(tab_name, section_id, in_table) {
-               var tasks = [], index = 0;
+       renderChildren(tab_name, section_id, in_table) {
+               const tasks = [];
+               let index = 0;
 
-               for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
+               for (let i = 0, opt; (opt = this.children[i]) != null; i++) {
                        if (opt.disable || opt.modalonly)
                                continue;
 
@@ -3410,10 +3423,10 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
        },
 
        /** @private */
-       renderTextValue: function(section_id, opt) {
-               var title = this.stripTags(opt.title).trim(),
-                   descr = this.stripTags(opt.description).trim(),
-                   value = opt.textvalue(section_id);
+       renderTextValue(section_id, opt) {
+               const title = this.stripTags(opt.title).trim();
+               const descr = this.stripTags(opt.description).trim();
+               const value = opt.textvalue(section_id);
 
                return E('td', {
                        'class': 'td cbi-value-field',
@@ -3425,23 +3438,23 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
        },
 
        /** @private */
-       renderHeaderRows: function(section_id) {
-               return this.super('renderHeaderRows', [ NaN, true ]);
+       renderHeaderRows(section_id) {
+               return this.super('renderHeaderRows', [ true ]);
        },
 
        /** @private */
-       renderRowActions: function(section_id) {
+       renderRowActions(section_id) {
                return this.super('renderRowActions', [ section_id, _('Edit') ]);
        },
 
        /** @override */
-       parse: function() {
-               var section_ids = this.cfgsections(),
-                   tasks = [];
+       parse() {
+               const section_ids = this.cfgsections();
+               const tasks = [];
 
                if (Array.isArray(this.children)) {
-                       for (var i = 0; i < section_ids.length; i++) {
-                               for (var j = 0; j < this.children.length; j++) {
+                       for (let i = 0; i < section_ids.length; i++) {
+                               for (let j = 0; j < this.children.length; j++) {
                                        if (!this.children[j].editable || this.children[j].modalonly)
                                                continue;
 
@@ -3483,10 +3496,10 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
  * @param {string} [description]
  * The description text of the form section element.
  */
-var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSection.prototype */ {
+const CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSection.prototype */ {
        __name__: 'CBI.NamedSection',
-       __init__: function(map, section_id /*, ... */) {
-               this.super('__init__', this.varargs(arguments, 2, map));
+       __init__(map, section_id, ...args) {
+               this.super('__init__', [ map, ...args ]);
 
                this.section = section_id;
        },
@@ -3520,39 +3533,41 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
         * @returns {string[]}
         * Returns a one-element array containing the mapped section ID.
         */
-       cfgsections: function() {
+       cfgsections() {
                return [ this.section ];
        },
 
        /** @private */
-       handleAdd: function(ev) {
-               var section_id = this.section,
-                   config_name = this.uciconfig || this.map.config;
+       handleAdd(ev) {
+               const section_id = this.section;
+               const config_name = this.uciconfig ?? this.map.config;
 
                this.map.data.add(config_name, this.sectiontype, section_id);
                return this.map.save(null, true);
        },
 
        /** @private */
-       handleRemove: function(ev) {
-               var section_id = this.section,
-                   config_name = this.uciconfig || this.map.config;
+       handleRemove(ev) {
+               const section_id = this.section;
+               const config_name = this.uciconfig ?? this.map.config;
 
                this.map.data.remove(config_name, section_id);
                return this.map.save(null, true);
        },
 
        /** @private */
-       renderContents: function(data) {
-               var ucidata = data[0], nodes = data[1],
-                   section_id = this.section,
-                   config_name = this.uciconfig || this.map.config,
-                   sectionEl = E('div', {
-                               'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
-                               'class': 'cbi-section',
-                               'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
-                               'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
-                       });
+       renderContents(data) {
+               const ucidata = data[0];
+               const nodes = data[1];
+               const section_id = this.section;
+               const config_name = this.uciconfig ?? this.map.config;
+
+               const sectionEl = E('div', {
+                       'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
+                       'class': 'cbi-section',
+                       'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
+                       'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
+               });
 
                if (typeof(this.title) === 'string' && this.title !== '')
                        sectionEl.appendChild(E('h3', {}, this.title));
@@ -3593,9 +3608,9 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
        },
 
        /** @override */
-       render: function() {
-               var config_name = this.uciconfig || this.map.config,
-                   section_id = this.section;
+       render() {
+               const config_name = this.uciconfig ?? this.map.config;
+               const section_id = this.section;
 
                return Promise.all([
                        this.map.data.get(config_name, section_id),
@@ -3636,7 +3651,7 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
+const CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
        __name__: 'CBI.Value',
 
        /**
@@ -3669,27 +3684,27 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
         * The caption for the choice value. May be a DOM node, a document fragment
         * or a plain text string. If omitted, the `key` value is used as caption.
         */
-       value: function(key, val) {
-               this.keylist = this.keylist || [];
+       value(key, val) {
+               this.keylist ??= [];
                this.keylist.push(String(key));
 
-               this.vallist = this.vallist || [];
+               this.vallist ??= [];
                this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
        },
 
        /** @override */
-       render: function(option_index, section_id, in_table) {
+       render(option_index, section_id, in_table) {
                return Promise.resolve(this.cfgvalue(section_id))
                        .then(this.renderWidget.bind(this, section_id, option_index))
                        .then(this.renderFrame.bind(this, section_id, in_table, option_index));
        },
 
        /** @private */
-       handleValueChange: function(section_id, state, ev) {
+       handleValueChange(section_id, state, ev) {
                if (typeof(this.onchange) != 'function')
                        return;
 
-               var value = this.formvalue(section_id);
+               const value = this.formvalue(section_id);
 
                if (isEqual(value, state.previousValue))
                        return;
@@ -3699,13 +3714,13 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
        },
 
        /** @private */
-       renderFrame: function(section_id, in_table, option_index, nodes) {
-               var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
-                   depend_list = this.transformDepList(section_id),
-                   optionEl;
+       renderFrame(section_id, in_table, option_index, nodes) {
+               const config_name = this.uciconfig ?? this.section.uciconfig ?? this.map.config;
+               const depend_list = this.transformDepList(section_id);
+               let optionEl;
 
                if (in_table) {
-                       var title = this.stripTags(this.title).trim();
+                       const title = this.stripTags(this.title).trim();
                        optionEl = E('td', {
                                'class': 'td cbi-value-field',
                                'data-title': (title != '') ? title : null,
@@ -3738,8 +3753,8 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
                                        'class': 'cbi-value-title',
                                        'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
                                        'click': function(ev) {
-                                               var node = ev.currentTarget,
-                                                   elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
+                                               const node = ev.currentTarget;
+                                               const elem = node.nextElementSibling.querySelector(`#${node.getAttribute('for')}`) ?? node.nextElementSibling.querySelector(`[data-widget-id="${node.getAttribute('for')}"]`);
 
                                                if (elem) {
                                                        elem.click();
@@ -3750,7 +3765,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
                                this.titleref ? E('a', {
                                        'class': 'cbi-title-ref',
                                        'href': this.titleref,
-                                       'title': this.titledesc || _('Go to relevant configuration page')
+                                       'title': this.titledesc ?? _('Go to relevant configuration page')
                                }, this.title) : this.title));
 
                                optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
@@ -3758,10 +3773,10 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
                }
 
                if (nodes)
-                       (optionEl.lastChild || optionEl).appendChild(nodes);
+                       (optionEl.lastChild ?? optionEl).appendChild(nodes);
 
                if (!in_table && typeof(this.description) === 'string' && this.description !== '')
-                       dom.append(optionEl.lastChild || optionEl,
+                       dom.append(optionEl.lastChild ?? optionEl,
                                E('div', { 'class': 'cbi-value-description' }, this.description.trim()));
 
                if (depend_list && depend_list.length)
@@ -3779,13 +3794,13 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
        },
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default,
-                   choices = this.transformChoices(),
-                   widget;
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
+               const choices = this.transformChoices();
+               let widget;
 
                if (choices) {
-                       var placeholder = (this.optional || this.rmempty)
+                       const placeholder = (this.optional || this.rmempty)
                                ? E('em', _('unspecified')) : _('-- Please choose --');
 
                        widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
@@ -3793,7 +3808,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
                                sort: this.keylist,
                                optional: this.optional || this.rmempty,
                                datatype: this.datatype,
-                               select_placeholder: this.placeholder || placeholder,
+                               select_placeholder: this.placeholder ?? placeholder,
                                validate: L.bind(this.validate, this, section_id),
                                disabled: (this.readonly != null) ? this.readonly : this.map.readonly
                        });
@@ -3846,16 +3861,16 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype */ {
+const CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype */ {
        __name__: 'CBI.DynamicList',
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default,
-                   choices = this.transformChoices(),
-                   items = L.toArray(value);
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
+               const choices = this.transformChoices();
+               const items = L.toArray(value);
 
-               var widget = new ui.DynamicList(items, choices, {
+               const widget = new ui.DynamicList(items, choices, {
                        id: this.cbid(section_id),
                        sort: this.keylist,
                        optional: this.optional || this.rmempty,
@@ -3901,11 +3916,11 @@ var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
+const CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
        __name__: 'CBI.ListValue',
 
-       __init__: function() {
-               this.super('__init__', arguments);
+       __init__(...args) {
+               this.super('__init__', args);
                this.widget = 'select';
                this.orientation = 'horizontal';
                this.deplist = [];
@@ -3943,9 +3958,9 @@ var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
         */
 
         /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var choices = this.transformChoices();
-               var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
+       renderWidget(section_id, option_index, cfgvalue) {
+               const choices = this.transformChoices();
+               const widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
                        id: this.cbid(section_id),
                        size: this.size,
                        sort: this.keylist,
@@ -3992,11 +4007,11 @@ var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
+const CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
        __name__: 'CBI.FlagValue',
 
-       __init__: function() {
-               this.super('__init__', arguments);
+       __init__(...args) {
+               this.super('__init__', args);
 
                this.enabled = '1';
                this.disabled = '0';
@@ -4045,20 +4060,20 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var tooltip = null;
+       renderWidget(section_id, option_index, cfgvalue) {
+               let tooltip = null;
 
                if (typeof(this.tooltip) == 'function')
-                       tooltip = this.tooltip.apply(this, [section_id]);
+                       tooltip = this.tooltip(section_id);
                else if (typeof(this.tooltip) == 'string')
-                       tooltip = (arguments.length > 1) ? ''.format.apply(this.tooltip, this.varargs(arguments, 1)) : this.tooltip;
+                       tooltip = this.tooltip.format(section_id);
 
-               var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
+               const widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
                        id: this.cbid(section_id),
                        value_enabled: this.enabled,
                        value_disabled: this.disabled,
                        validate: L.bind(this.validate, this, section_id),
-                       tooltip: tooltip,
+                       tooltip,
                        tooltipicon: this.tooltipicon,
                        disabled: (this.readonly != null) ? this.readonly : this.map.readonly
                });
@@ -4073,9 +4088,9 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
         *
         * @override
         */
-       formvalue: function(section_id) {
-               var elem = this.getUIElement(section_id),
-                   checked = elem ? elem.isChecked() : false;
+       formvalue(section_id) {
+               const elem = this.getUIElement(section_id);
+               const checked = elem ? elem.isChecked() : false;
                return checked ? this.enabled : this.disabled;
        },
 
@@ -4085,8 +4100,8 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
         *
         * @override
         */
-       textvalue: function(section_id) {
-               var cval = this.cfgvalue(section_id);
+       textvalue(section_id) {
+               let cval = this.cfgvalue(section_id);
 
                if (cval == null)
                        cval = this.default;
@@ -4095,15 +4110,16 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
        },
 
        /** @override */
-       parse: function(section_id) {
+       parse(section_id) {
                if (this.isActive(section_id)) {
-                       var fval = this.formvalue(section_id);
+                       const fval = this.formvalue(section_id);
 
                        if (!this.isValid(section_id)) {
-                               var title = this.stripTags(this.title).trim();
-                               var error = this.getValidationError(section_id);
+                               const title = this.stripTags(this.title).trim();
+                               const error = this.getValidationError(section_id);
+
                                return Promise.reject(new TypeError(
-                                       _('Option "%s" contains an invalid input value.').format(title || this.option) + ' ' + error));
+                                       `${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${error}`));
                        }
 
                        if (fval == this.default && (this.optional || this.rmempty))
@@ -4149,11 +4165,11 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prototype */ {
+const CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prototype */ {
        __name__: 'CBI.MultiValue',
 
-       __init__: function() {
-               this.super('__init__', arguments);
+       __init__(...args) {
+               this.super('__init__', args);
                this.placeholder = _('-- Please choose --');
        },
 
@@ -4178,18 +4194,18 @@ var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.protot
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default,
-                   choices = this.transformChoices();
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
+               const choices = this.transformChoices();
 
-               var widget = new ui.Dropdown(L.toArray(value), choices, {
+               const widget = new ui.Dropdown(L.toArray(value), choices, {
                        id: this.cbid(section_id),
                        sort: this.keylist,
                        multiple: true,
                        optional: this.optional || this.rmempty,
                        select_placeholder: this.placeholder,
-                       display_items: this.display_size || this.size || 3,
-                       dropdown_items: this.dropdown_size || this.size || -1,
+                       display_items: this.display_size ?? this.size ?? 3,
+                       dropdown_items: this.dropdown_size ?? this.size ?? -1,
                        validate: L.bind(this.validate, this, section_id),
                        disabled: (this.readonly != null) ? this.readonly : this.map.readonly
                });
@@ -4229,7 +4245,7 @@ var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.protot
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
+const CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
        __name__: 'CBI.TextValue',
 
        /** @ignore */
@@ -4272,10 +4288,10 @@ var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default;
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
 
-               var widget = new ui.Textarea(value, {
+               const widget = new ui.Textarea(value, {
                        id: this.cbid(section_id),
                        optional: this.optional || this.rmempty,
                        placeholder: this.placeholder,
@@ -4322,7 +4338,7 @@ var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */ {
+const CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */ {
        __name__: 'CBI.DummyValue',
 
        /**
@@ -4348,7 +4364,7 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
         * @default null
         */
 
-    /**
+       /**
         * Render the UCI option value as hidden using the HTML display: none style property.
         *
         * By default, the value is displayed
@@ -4359,15 +4375,15 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default,
-                   hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
-                   outputEl = E('div', { 'style': this.hidden ? 'display:none' : null });
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
+               const hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) });
+               const outputEl = E('div', { 'style': this.hidden ? 'display:none' : null });
 
                if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly))
                        outputEl.appendChild(E('a', { 'href': this.href }));
 
-               dom.append(outputEl.lastChild || outputEl,
+               dom.append(outputEl.lastChild ?? outputEl,
                        this.rawhtml ? value : [ value ]);
 
                return E([
@@ -4377,10 +4393,10 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
        },
 
        /** @override */
-       remove: function() {},
+       remove() {},
 
        /** @override */
-       write: function() {}
+       write() {}
 });
 
 /**
@@ -4414,7 +4430,7 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype */ {
+const CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype */ {
        __name__: 'CBI.ButtonValue',
 
        /**
@@ -4469,16 +4485,16 @@ var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var value = (cfgvalue != null) ? cfgvalue : this.default,
-                   hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
-                   outputEl = E('div'),
-                   btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
+       renderWidget(section_id, option_index, cfgvalue) {
+               const value = (cfgvalue != null) ? cfgvalue : this.default;
+               const hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) });
+               const outputEl = E('div');
+               const btn_title = this.titleFn('inputtitle', section_id) ?? this.titleFn('title', section_id);
 
                if (value !== false)
                        dom.content(outputEl, [
                                E('button', {
-                                       'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
+                                       'class': 'cbi-button cbi-button-%s'.format(this.inputstyle ?? 'button'),
                                        'click': ui.createHandlerFn(this, function(section_id, ev) {
                                                if (this.onclick)
                                                        return this.onclick(ev, section_id);
@@ -4486,7 +4502,7 @@ var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype
                                                ev.currentTarget.parentNode.nextElementSibling.value = value;
                                                return this.map.save();
                                        }, section_id),
-                                       'disabled': ((this.readonly != null) ? this.readonly : this.map.readonly) || null
+                                       'disabled': (this.readonly ?? this.map.readonly) || null
                                }, [ btn_title ])
                        ]);
                else
@@ -4537,12 +4553,12 @@ var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIHiddenValue = CBIValue.extend(/** @lends LuCI.form.HiddenValue.prototype */ {
+const CBIHiddenValue = CBIValue.extend(/** @lends LuCI.form.HiddenValue.prototype */ {
        __name__: 'CBI.HiddenValue',
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
+       renderWidget(section_id, option_index, cfgvalue) {
+               const widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
                        id: this.cbid(section_id)
                });
 
@@ -4581,11 +4597,11 @@ var CBIHiddenValue = CBIValue.extend(/** @lends LuCI.form.HiddenValue.prototype
  * @param {string} [description]
  * The description text of the option element.
  */
-var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */ {
+const CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */ {
        __name__: 'CBI.FileSelect',
 
-       __init__: function(/* ... */) {
-               this.super('__init__', arguments);
+       __init__(...args) {
+               this.super('__init__', args);
 
                this.show_hidden = false;
                this.enable_upload = true;
@@ -4654,8 +4670,8 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
         */
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
-               var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
+       renderWidget(section_id, option_index, cfgvalue) {
+               const browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
                        id: this.cbid(section_id),
                        name: this.cbid(section_id),
                        show_hidden: this.show_hidden,
@@ -4707,15 +4723,15 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
  * All further arguments are passed as-is to the subclass constructor. Refer
  * to the corresponding class constructor documentations for details.
  */
-var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototype */ {
+const CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototype */ {
        __name__: 'CBI.ContainerValue',
-       __init__: function(map, section, option, cbiClass /*, ... */) {
-               this.super('__init__', [map, section, option]);
+       __init__(map, section, option, cbiClass, ...args) {
+               this.super('__init__', [ map, section, option ]);
 
                if (!CBIAbstractSection.isSubclass(cbiClass))
                        throw 'Sub section must be a descendent of CBIAbstractSection';
 
-               this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
+               this.subsection = cbiClass.instantiate([ this.map, ...args ]);
                this.subsection.parentoption = this;
        },
 
@@ -4730,22 +4746,22 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         */
 
        /** @override */
-       load: function(section_id) {
+       load(section_id) {
                return this.subsection.load(section_id);
        },
 
        /** @override */
-       parse: function(section_id) {
+       parse(section_id) {
                return this.subsection.parse(section_id);
        },
 
        /** @private */
-       renderWidget: function(section_id, option_index, cfgvalue) {
+       renderWidget(section_id, option_index, cfgvalue) {
                return this.subsection.render(section_id);
        },
 
        /** @private */
-       checkDepends: function(section_id) {
+       checkDepends(section_id) {
                this.subsection.checkDepends(section_id);
                return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
        },
@@ -4756,7 +4772,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         *
         * @override
         */
-       value: function() {},
+       value() {},
 
        /**
         * Since the section container is not tied to any UCI configuration,
@@ -4764,7 +4780,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         *
         * @override
         */
-       write: function() {},
+       write() {},
 
        /**
         * Since the section container is not tied to any UCI configuration,
@@ -4772,7 +4788,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         *
         * @override
         */
-       remove: function() {},
+       remove() {},
 
        /**
         * Since the section container is not tied to any UCI configuration,
@@ -4781,7 +4797,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         * @override
         * @returns {null}
         */
-       cfgvalue: function() { return null },
+       cfgvalue() { return null },
 
        /**
         * Since the section container is not tied to any UCI configuration,
@@ -4790,7 +4806,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
         * @override
         * @returns {null}
         */
-       formvalue: function() { return null }
+       formvalue() { return null }
 });
 
 /**
@@ -4817,7 +4833,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
  * 'use strict';
  * 'require form';
  *
- * var m, s, o;
+ * let m, s, o;
  *
  * m = new form.Map('example', 'Example form',
  *     'This is an example form mapping the contents of /etc/config/example');
index ca0e80a82a6be4b8df70a3514a11b984104d48ed..c1b1efdadf3ca2360e4c3b5b5ad9c16d9217719a 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, options).then(function(res) {
-                                               if (!Poll.active())
-                                                       return;
+                               const fn = () => Request.request(url, options).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]))
                                        url.push('/', 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);
index 801006659448df6e7414a20076366fa9eff2c09a..903d9d0b6079f2e50acf127f899d0265903fe9ad 100644 (file)
@@ -2,10 +2,10 @@
 'require baseclass';
 'require request';
 
-var rpcRequestID = 1,
-    rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
-    rpcBaseURL = L.url('admin/ubus'),
-    rpcInterceptorFns = [];
+let rpcRequestID = 1;
+let rpcSessionID = L.env.sessionid ?? '00000000000000000000000000000000';
+let rpcBaseURL = L.url('admin/ubus');
+const rpcInterceptorFns = [];
 
 /**
  * @class rpc
@@ -18,14 +18,14 @@ var rpcRequestID = 1,
  */
 return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
        /* privates */
-       call: function(req, cb, nobatch) {
-               var q = '';
+       call(req, cb, nobatch) {
+               let q = '';
 
                if (Array.isArray(req)) {
                        if (req.length == 0)
                                return Promise.resolve([]);
 
-                       for (var i = 0; i < req.length; i++)
+                       for (let i = 0; i < req.length; i++)
                                if (req[i].params)
                                        q += '%s%s.%s'.format(
                                                q ? ';' : '/',
@@ -35,14 +35,14 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
                }
 
                return request.post(rpcBaseURL + q, req, {
-                       timeout: (L.env.rpctimeout || 20) * 1000,
-                       nobatch: nobatch,
+                       timeout: (L.env.rpctimeout ?? 20) * 1000,
+                       nobatch,
                        credentials: true
                }).then(cb, cb);
        },
 
-       parseCallReply: function(req, res) {
-               var msg = null;
+       parseCallReply(req, res) {
+               let msg = null;
 
                if (res instanceof Error)
                        return req.reject(res);
@@ -62,14 +62,14 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
                 * The interceptor args are intentionally swapped.
                 * Response is passed as first arg to align with Request class interceptors
                 */
-               Promise.all(rpcInterceptorFns.map(function(fn) { return fn(msg, req) }))
+               Promise.all(rpcInterceptorFns.map(fn => fn(msg, req)))
                        .then(this.handleCallReply.bind(this, req, msg))
                        .catch(req.reject);
        },
 
-       handleCallReply: function(req, msg) {
-               var type = Object.prototype.toString,
-                   ret = null;
+       handleCallReply(req, msg) {
+               const type = Object.prototype.toString;
+               let ret = null;
 
                try {
                        /* verify message frame */
@@ -98,7 +98,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
                }
 
                if (req.expect) {
-                       for (var key in req.expect) {
+                       for (const key in req.expect) {
                                if (ret != null && key != '')
                                        ret = ret[key];
 
@@ -140,20 +140,17 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * more arguments, a promise resolving to an object describing the method
         * signatures of each requested `ubus` object name will be returned.
         */
-       list: function() {
-               var msg = {
+       list(...args) {
+               const msg = {
                        jsonrpc: '2.0',
                        id:      rpcRequestID++,
                        method:  'list',
-                       params:  arguments.length ? this.varargs(arguments) : undefined
+                       params:  args.length ? args : undefined
                };
 
-               return new Promise(L.bind(function(resolveFn, rejectFn) {
+               return new Promise(L.bind(function(resolve, reject) {
                        /* store request info */
-                       var req = {
-                               resolve: resolveFn,
-                               reject:  rejectFn
-                       };
+                       const req = { resolve, reject };
 
                        /* call rpc */
                        this.call(msg, this.parseCallReply.bind(this, req));
@@ -296,37 +293,36 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * Returns a new function implementing the method call described in
         * `options`.
         */
-       declare: function(options) {
-               return Function.prototype.bind.call(function(rpc, options) {
-                       var args = this.varargs(arguments, 2);
-                       return new Promise(function(resolveFn, rejectFn) {
+       declare(options) {
+               return Function.prototype.bind.call(function(rpc, options, ...args) {
+                       return new Promise((resolve, reject) => {
                                /* build parameter object */
-                               var p_off = 0;
-                               var params = { };
+                               let p_off = 0;
+                               const params = { };
                                if (Array.isArray(options.params))
                                        for (p_off = 0; p_off < options.params.length; p_off++)
                                                params[options.params[p_off]] = args[p_off];
 
                                /* all remaining arguments are private args */
-                               var priv = [ undefined, undefined ];
+                               const priv = [ undefined, undefined ];
                                for (; p_off < args.length; p_off++)
                                        priv.push(args[p_off]);
 
                                /* store request info */
-                               var req = {
+                               const req = {
                                        expect:  options.expect,
                                        filter:  options.filter,
-                                       resolve: resolveFn,
-                                       reject:  rejectFn,
-                                       params:  params,
-                                       priv:    priv,
+                                       resolve,
+                                       reject,
+                                       params,
+                                       priv,
                                        object:  options.object,
                                        method:  options.method,
                                        raise:   options.reject
                                };
 
                                /* build message object */
-                               var msg = {
+                               const msg = {
                                        jsonrpc: '2.0',
                                        id:      rpcRequestID++,
                                        method:  'call',
@@ -351,7 +347,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * Returns the 32 byte session ID string used for authenticating remote
         * requests.
         */
-       getSessionID: function() {
+       getSessionID() {
                return rpcSessionID;
        },
 
@@ -362,7 +358,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * Sets the 32 byte session ID string used for authenticating remote
         * requests.
         */
-       setSessionID: function(sid) {
+       setSessionID(sid) {
                rpcSessionID = sid;
        },
 
@@ -372,7 +368,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * @returns {string}
         * Returns the RPC URL endpoint to issue requests against.
         */
-       getBaseURL: function() {
+       getBaseURL() {
                return rpcBaseURL;
        },
 
@@ -382,7 +378,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * @param {string} url
         * Sets the RPC URL endpoint to issue requests against.
         */
-       setBaseURL: function(url) {
+       setBaseURL(url) {
                rpcBaseURL = url;
        },
 
@@ -396,7 +392,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * @returns {string}
         * Returns the textual description of the code.
         */
-       getStatusText: function(statusCode) {
+       getStatusText(statusCode) {
                switch (statusCode) {
                case 0: return _('Command OK');
                case 1: return _('Invalid command');
@@ -459,9 +455,10 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * @returns {LuCI.rpc~interceptorFn}
         * Returns the given function value.
         */
-       addInterceptor: function(interceptorFn) {
+       addInterceptor(interceptorFn) {
                if (typeof(interceptorFn) == 'function')
                        rpcInterceptorFns.push(interceptorFn);
+
                return interceptorFn;
        },
 
@@ -475,11 +472,14 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
         * Returns `true` if the given function has been removed or `false`
         * if it has not been found.
         */
-       removeInterceptor: function(interceptorFn) {
-               var oldlen = rpcInterceptorFns.length, i = oldlen;
+       removeInterceptor(interceptorFn) {
+               const oldlen = rpcInterceptorFns.length;
+               let i = oldlen;
+
                while (i--)
                        if (rpcInterceptorFns[i] === interceptorFn)
                                rpcInterceptorFns.splice(i, 1);
+
                return (rpcInterceptorFns.length < oldlen);
        }
 });
index 76b274470b19dd6c9698e95b341b80e48b92be3a..758fb284fbfb6e689eb16ae46cf03a7fbc65269b 100644 (file)
@@ -3,7 +3,7 @@
 'require baseclass';
 
 function isEmpty(object, ignore) {
-       for (var property in object)
+       for (const property in object)
                if (object.hasOwnProperty(property) && property != ignore)
                        return false;
 
@@ -22,7 +22,7 @@ function isEmpty(object, ignore) {
  * UCI configuration data.
  */
 return baseclass.extend(/** @lends LuCI.uci.prototype */ {
-       __init__: function() {
+       __init__() {
                this.state = {
                        newidx:  0,
                        values:  { },
@@ -100,14 +100,14 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * A newly generated, unique section ID in the form `newXXXXXX`
         * where `X` denotes a hexadecimal digit.
         */
-       createSID: function(conf) {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   sid;
+       createSID(conf) {
+               const v = this.state.values;
+               const n = this.state.creates;
+               let sid;
 
                do {
                        sid = "new%06x".format(Math.random() * 0xFFFFFF);
-               } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
+               } while ((n[conf]?.[sid]) || (v[conf]?.[sid]));
 
                return sid;
        },
@@ -131,30 +131,30 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * not in extended notation. Returns `null` when an extended ID could
         * not be resolved to existing section ID.
         */
-       resolveSID: function(conf, sid) {
+       resolveSID(conf, sid) {
                if (typeof(sid) != 'string')
                        return sid;
 
-               var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
+               const m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
 
                if (m) {
-                       var type = m[1],
-                           pos = +m[2],
-                           sections = this.sections(conf, type),
-                           section = sections[pos >= 0 ? pos : sections.length + pos];
+                       const type = m[1];
+                       const pos = +m[2];
+                       const sections = this.sections(conf, type);
+                       const section = sections[pos >= 0 ? pos : sections.length + pos];
 
-                       return section ? section['.name'] : null;
+                       return section?.['.name'] ?? null;
                }
 
                return sid;
        },
 
        /* private */
-       reorderSections: function() {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   r = this.state.reorder,
-                   tasks = [];
+       reorderSections() {
+               const v = this.state.values;
+               const n = this.state.creates;
+               const r = this.state.reorder;
+               const tasks = [];
 
                if (Object.keys(r).length === 0)
                        return Promise.resolve();
@@ -163,24 +163,22 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                 gather all created and existing sections, sort them according
                 to their index value and issue an uci order call
                */
-               for (var c in r) {
-                       var o = [ ];
+               for (const c in r) {
+                       const o = [ ];
 
                        if (n[c])
-                               for (var s in n[c])
+                               for (const s in n[c])
                                        o.push(n[c][s]);
 
-                       for (var s in v[c])
+                       for (const s in v[c])
                                o.push(v[c][s]);
 
                        if (o.length > 0) {
-                               o.sort(function(a, b) {
-                                       return (a['.index'] - b['.index']);
-                               });
+                               o.sort((a, b) => a['.index'] - b['.index']);
 
-                               var sids = [ ];
+                               const sids = [ ];
 
-                               for (var i = 0; i < o.length; i++)
+                               for (let i = 0; i < o.length; i++)
                                        sids.push(o[i]['.name']);
 
                                tasks.push(this.callOrder(c, sids));
@@ -192,7 +190,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
        },
 
        /* private */
-       loadPackage: function(packageName) {
+       loadPackage(packageName) {
                if (this.loaded[packageName] == null)
                        return (this.loaded[packageName] = this.callLoad(packageName));
 
@@ -217,22 +215,22 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * Returns a promise resolving to the names of the configurations
         * that have been successfully loaded.
         */
-       load: function(packages) {
-               var self = this,
-                   pkgs = [ ],
-                   tasks = [];
+       load(packages) {
+               const self = this;
+               const pkgs = [ ];
+               const tasks = [];
 
                if (!Array.isArray(packages))
                        packages = [ packages ];
 
-               for (var i = 0; i < packages.length; i++)
+               for (let i = 0; i < packages.length; i++)
                        if (!self.state.values[packages[i]]) {
                                pkgs.push(packages[i]);
                                tasks.push(self.loadPackage(packages[i]));
                        }
 
-               return Promise.all(tasks).then(function(responses) {
-                       for (var i = 0; i < responses.length; i++)
+               return Promise.all(tasks).then(responses => {
+                       for (let i = 0; i < responses.length; i++)
                                self.state.values[pkgs[i]] = responses[i];
 
                        if (responses.length)
@@ -249,11 +247,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * The name of the configuration or an array of configuration
         * names to unload.
         */
-       unload: function(packages) {
+       unload(packages) {
                if (!Array.isArray(packages))
                        packages = [ packages ];
 
-               for (var i = 0; i < packages.length; i++) {
+               for (let i = 0; i < packages.length; i++) {
                        delete this.state.values[packages[i]];
                        delete this.state.creates[packages[i]];
                        delete this.state.changes[packages[i]];
@@ -281,13 +279,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * Returns the section ID of the newly added section which is equivalent
         * to the given name for non-anonymous sections.
         */
-       add: function(conf, type, name) {
-               var n = this.state.creates,
-                   sid = name || this.createSID(conf);
-
-               if (!n[conf])
-                       n[conf] = { };
+       add(conf, type, name) {
+               const n = this.state.creates;
+               const sid = name || this.createSID(conf);
 
+               n[conf] ??= { };
                n[conf][sid] = {
                        '.type':      type,
                        '.name':      sid,
@@ -308,23 +304,20 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * @param {string} sid
         * The ID of the section to remove.
         */
-       remove: function(conf, sid) {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   c = this.state.changes,
-                   d = this.state.deletes;
+       remove(conf, sid) {
+               const v = this.state.values;
+               const n = this.state.creates;
+               const c = this.state.changes;
+               const d = this.state.deletes;
 
                /* requested deletion of a just created section */
-               if (n[conf] && n[conf][sid]) {
+               if (n[conf]?.[sid]) {
                        delete n[conf][sid];
                }
-               else if (v[conf] && v[conf][sid]) {
-                       if (c[conf])
-                               delete c[conf][sid];
-
-                       if (!d[conf])
-                               d[conf] = { };
+               else if (v[conf]?.[sid]) {
+                       delete c[conf]?.[sid];
 
+                       d[conf] ??= { };
                        d[conf][sid] = true;
                }
        },
@@ -397,35 +390,35 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * Returns a sorted array of the section objects within the given
         * configuration, filtered by type of a type has been specified.
         */
-       sections: function(conf, type, cb) {
-               var sa = [ ],
-                   v = this.state.values[conf],
-                   n = this.state.creates[conf],
-                   c = this.state.changes[conf],
-                   d = this.state.deletes[conf];
+       sections(conf, type, cb) {
+               const sa = [ ];
+               const v = this.state.values[conf];
+               const n = this.state.creates[conf];
+               const c = this.state.changes[conf];
+               const d = this.state.deletes[conf];
 
                if (!v)
                        return sa;
 
-               for (var s in v)
+               for (const s in v)
                        if (!d || d[s] !== true)
                                if (!type || v[s]['.type'] == type)
                                        sa.push(Object.assign({ }, v[s], c ? c[s] : null));
 
                if (n)
-                       for (var s in n)
+                       for (const s in n)
                                if (!type || n[s]['.type'] == type)
                                        sa.push(Object.assign({ }, n[s]));
 
-               sa.sort(function(a, b) {
+               sa.sort((a, b) => {
                        return a['.index'] - b['.index'];
                });
 
-               for (var i = 0; i < sa.length; i++)
+               for (let i = 0; i < sa.length; i++)
                        sa[i]['.index'] = i;
 
                if (typeof(cb) == 'function')
-                       for (var i = 0; i < sa.length; i++)
+                       for (let i = 0; i < sa.length; i++)
                                cb.call(this, sa[i], sa[i]['.name']);
 
                return sa;
@@ -456,11 +449,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * - Returns `null` if the config, section or option has not been
         *   found or if the corresponding configuration is not loaded.
         */
-       get: function(conf, sid, opt) {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   c = this.state.changes,
-                   d = this.state.deletes;
+       get(conf, sid, opt) {
+               const v = this.state.values;
+               const n = this.state.creates;
+               const c = this.state.changes;
+               const d = this.state.deletes;
 
                sid = this.resolveSID(conf, sid);
 
@@ -468,10 +461,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                        return null;
 
                /* requested option in a just created section */
-               if (n[conf] && n[conf][sid]) {
-                       if (!n[conf])
-                               return null;
-
+               if (n[conf]?.[sid]) {
                        if (opt == null)
                                return n[conf][sid];
 
@@ -481,16 +471,16 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                /* requested an option value */
                if (opt != null) {
                        /* check whether option was deleted */
-                       if (d[conf] && d[conf][sid])
+                       if (d[conf]?.[sid])
                                if (d[conf][sid] === true || d[conf][sid][opt])
                                        return null;
 
                        /* check whether option was changed */
-                       if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
+                       if (c[conf]?.[sid][opt] != null)
                                return c[conf][sid][opt];
 
                        /* return base value */
-                       if (v[conf] && v[conf][sid])
+                       if (v[conf]?.[sid])
                                return v[conf][sid][opt];
 
                        return null;
@@ -499,21 +489,21 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                /* requested an entire section */
                if (v[conf]) {
                        /* check whether entire section was deleted */
-                       if (d[conf] && d[conf][sid] === true)
+                       if (d[conf]?.[sid] === true)
                                return null;
 
-                       var s = v[conf][sid] || null;
+                       const s = v[conf][sid] || null;
 
                        if (s) {
                                /* merge changes */
-                               if (c[conf] && c[conf][sid])
-                                       for (var opt in c[conf][sid])
+                               if (c[conf]?.[sid])
+                                       for (const opt in c[conf][sid])
                                                if (c[conf][sid][opt] != null)
                                                        s[opt] = c[conf][sid][opt];
 
                                /* merge deletions */
-                               if (d[conf] && d[conf][sid])
-                                       for (var opt in d[conf][sid])
+                               if (d[conf]?.[sid])
+                                       for (const opt in d[conf][sid])
                                                delete s[opt];
                        }
 
@@ -544,18 +534,18 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * the option will be removed, otherwise it will be set or overwritten
         * with the given value.
         */
-       set: function(conf, sid, opt, val) {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   c = this.state.changes,
-                   d = this.state.deletes;
+       set(conf, sid, opt, val) {
+               const v = this.state.values;
+               const n = this.state.creates;
+               const c = this.state.changes;
+               const d = this.state.deletes;
 
                sid = this.resolveSID(conf, sid);
 
                if (sid == null || opt == null || opt.charAt(0) == '.')
                        return;
 
-               if (n[conf] && n[conf][sid]) {
+               if (n[conf]?.[sid]) {
                        if (val != null)
                                n[conf][sid][opt] = val;
                        else
@@ -567,17 +557,14 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                                return;
 
                        /* only set in existing sections */
-                       if (!v[conf] || !v[conf][sid])
+                       if (!v[conf]?.[sid])
                                return;
 
-                       if (!c[conf])
-                               c[conf] = {};
-
-                       if (!c[conf][sid])
-                               c[conf][sid] = {};
+                       c[conf] ??= {};
+                       c[conf][sid] ??= {};
 
                        /* undelete option */
-                       if (d[conf] && d[conf][sid]) {
+                       if (d[conf]?.[sid]) {
                                if (isEmpty(d[conf][sid], opt))
                                        delete d[conf][sid];
                                else
@@ -588,7 +575,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                }
                else {
                        /* revert any change for to-be-deleted option */
-                       if (c[conf] && c[conf][sid]) {
+                       if (c[conf]?.[sid]) {
                                if (isEmpty(c[conf][sid], opt))
                                        delete c[conf][sid];
                                else
@@ -596,12 +583,9 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                        }
 
                        /* only delete existing options */
-                       if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) {
-                               if (!d[conf])
-                                       d[conf] = { };
-
-                               if (!d[conf][sid])
-                                       d[conf][sid] = { };
+                       if (v[conf]?.[sid].hasOwnProperty(opt)) {
+                               d[conf] ??= { };
+                               d[conf][sid] ??= { };
 
                                if (d[conf][sid] !== true)
                                        d[conf][sid][opt] = true;
@@ -625,7 +609,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * @param {string} opt
         * The name of the option to remove.
         */
-       unset: function(conf, sid, opt) {
+       unset(conf, sid, opt) {
                return this.set(conf, sid, opt, null);
        },
 
@@ -656,12 +640,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * - Returns `null` if the config, section or option has not been
         *   found or if the corresponding configuration is not loaded.
         */
-       get_first: function(conf, type, opt) {
-               var sid = null;
+       get_first(conf, type, opt) {
+               let sid = null;
 
-               this.sections(conf, type, function(s) {
-                       if (sid == null)
-                               sid = s['.name'];
+               this.sections(conf, type, s => {
+                       sid ??= s['.name'];
                });
 
                return this.get(conf, sid, opt);
@@ -691,12 +674,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * the option will be removed, otherwise it will be set or overwritten
         * with the given value.
         */
-       set_first: function(conf, type, opt, val) {
-               var sid = null;
+       set_first(conf, type, opt, val) {
+               let sid = null;
 
-               this.sections(conf, type, function(s) {
-                       if (sid == null)
-                               sid = s['.name'];
+               this.sections(conf, type, s => {
+                       sid ??= s['.name'];
                });
 
                return this.set(conf, sid, opt, val);
@@ -721,7 +703,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * @param {string} opt
         * The option name to set the value for.
         */
-       unset_first: function(conf, type, opt) {
+       unset_first(conf, type, opt) {
                return this.set_first(conf, type, opt, null);
        },
 
@@ -756,14 +738,15 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * Returns `true` when the section was successfully moved, or `false`
         * when either the section specified by `sid1` or by `sid2` is not found.
         */
-       move: function(conf, sid1, sid2, after) {
-               var sa = this.sections(conf),
-                   s1 = null, s2 = null;
+       move(conf, sid1, sid2, after) {
+               const sa = this.sections(conf);
+               let s1 = null;
+               let s2 = null;
 
                sid1 = this.resolveSID(conf, sid1);
                sid2 = this.resolveSID(conf, sid2);
 
-               for (var i = 0; i < sa.length; i++) {
+               for (let i = 0; i < sa.length; i++) {
                        if (sa[i]['.name'] != sid1)
                                continue;
 
@@ -779,7 +762,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                        sa.push(s1);
                }
                else {
-                       for (var i = 0; i < sa.length; i++) {
+                       for (let i = 0; i < sa.length; i++) {
                                if (sa[i]['.name'] != sid2)
                                        continue;
 
@@ -792,7 +775,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                                return false;
                }
 
-               for (var i = 0; i < sa.length; i++)
+               for (let i = 0; i < sa.length; i++)
                        this.get(conf, sa[i]['.name'])['.index'] = i;
 
                this.state.reorder[conf] = true;
@@ -810,21 +793,21 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * Returns a promise resolving to an array of configuration names which
         * have been reloaded by the save operation.
         */
-       save: function() {
-               var v = this.state.values,
-                   n = this.state.creates,
-                   c = this.state.changes,
-                   d = this.state.deletes,
-                   r = this.state.reorder,
-                   self = this,
-                   snew = [ ],
-                   pkgs = { },
-                   tasks = [];
+       save() {
+               const v = this.state.values;
+               const n = this.state.creates;
+               const c = this.state.changes;
+               const d = this.state.deletes;
+               const r = this.state.reorder;
+               const self = this;
+               const snew = [ ];
+               let pkgs = { };
+               const tasks = [];
 
                if (d)
-                       for (var conf in d) {
-                               for (var sid in d[conf]) {
-                                       var o = d[conf][sid];
+                       for (const conf in d) {
+                               for (const sid in d[conf]) {
+                                       const o = d[conf][sid];
 
                                        if (o === true)
                                                tasks.push(self.callDelete(conf, sid, null));
@@ -836,14 +819,14 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                        }
 
                if (n)
-                       for (var conf in n) {
-                               for (var sid in n[conf]) {
-                                       var p = {
+                       for (const conf in n) {
+                               for (const sid in n[conf]) {
+                                       const p = {
                                                config: conf,
                                                values: { }
                                        };
 
-                                       for (var k in n[conf][sid]) {
+                                       for (const k in n[conf][sid]) {
                                                if (k == '.type')
                                                        p.type = n[conf][sid][k];
                                                else if (k == '.create')
@@ -860,27 +843,27 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                        }
 
                if (c)
-                       for (var conf in c) {
-                               for (var sid in c[conf])
+                       for (const conf in c) {
+                               for (const sid in c[conf])
                                        tasks.push(self.callSet(conf, sid, c[conf][sid]));
 
                                pkgs[conf] = true;
                        }
 
                if (r)
-                       for (var conf in r)
+                       for (const conf in r)
                                pkgs[conf] = true;
 
-               return Promise.all(tasks).then(function(responses) {
+               return Promise.all(tasks).then(responses => {
                        /*
                         array "snew" holds references to the created uci sections,
                         use it to assign the returned names of the new sections
                        */
-                       for (var i = 0; i < snew.length; i++)
+                       for (let i = 0; i < snew.length; i++)
                                snew[i]['.name'] = responses[i];
 
                        return self.reorderSections();
-               }).then(function() {
+               }).then(() => {
                        pkgs = Object.keys(pkgs);
 
                        self.unload(pkgs);
@@ -900,20 +883,20 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * @returns {Promise<number>}
         * Returns a promise resolving/rejecting with the `ubus` RPC status code.
         */
-       apply: function(timeout) {
-               var self = this,
-                   date = new Date();
+       apply(timeout) {
+               const self = this;
+               const date = new Date();
 
                if (typeof(timeout) != 'number' || timeout < 1)
                        timeout = 10;
 
-               return self.callApply(timeout, true).then(function(rv) {
+               return self.callApply(timeout, true).then(rv => {
                        if (rv != 0)
                                return Promise.reject(rv);
 
-                       var try_deadline = date.getTime() + 1000 * timeout;
-                       var try_confirm = function() {
-                               return self.callConfirm().then(function(rv) {
+                       const try_deadline = date.getTime() + 1000 * timeout;
+                       const try_confirm = () => {
+                               return self.callConfirm().then(rv => {
                                        if (rv != 0) {
                                                if (date.getTime() < try_deadline)
                                                        window.setTimeout(try_confirm, 250);
index 1580996ef419f7b5dfed6f2a814c71cfc6b42dab..7e6c4748b760f723d07c2c9cde43a700cee365cb 100644 (file)
@@ -9,10 +9,10 @@
 'require uci';
 'require fs';
 
-var modalDiv = null,
-    tooltipDiv = null,
-    indicatorDiv = null,
-    tooltipTimeout = null;
+let modalDiv = null;
+let tooltipDiv = null;
+let indicatorDiv = null;
+let tooltipTimeout = null;
 
 /**
  * @class AbstractElement
@@ -34,7 +34,7 @@ var modalDiv = null,
  * it in external JavaScript, use `L.require("ui").then(...)` and access the
  * `AbstractElement` property of the class instance value.
  */
-var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
+const UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
        /**
         * @typedef {Object} InitOptions
         * @memberof LuCI.ui.AbstractElement
@@ -78,7 +78,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * string. Complex widgets such as `DynamicList` instances may result in
         * an array of strings or `null` for unset values.
         */
-       getValue: function() {
+       getValue() {
                if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
                        return this.node.value;
 
@@ -96,7 +96,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * Complex widgets such as `DynamicList` instances may accept string array
         * or `null` values.
         */
-       setValue: function(value) {
+       setValue(value) {
                if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
                        this.node.value = value;
        },
@@ -110,10 +110,10 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * The placeholder to set for the input element. Only applicable to text
         * inputs, not to radio buttons, selects or similar.
         */
-       setPlaceholder: function(value) {
-               var node = this.node ? this.node.querySelector('input,textarea') : null;
+       setPlaceholder(value) {
+               const node = this.node ? this.node.querySelector('input,textarea') : null;
                if (node) {
-                       switch (node.getAttribute('type') || 'text') {
+                       switch (node.getAttribute('type') ?? 'text') {
                        case 'password':
                        case 'search':
                        case 'tel':
@@ -138,7 +138,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * value and changes it back to the original state, it is still reported
         * as changed.
         */
-       isChanged: function() {
+       isChanged() {
                return (this.node ? this.node.getAttribute('data-changed') : null) == 'true';
        },
 
@@ -151,7 +151,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * Returns `true` if the current input value is valid or `false` if it does
         * not meet the validation constraints.
         */
-       isValid: function() {
+       isValid() {
                return (this.validState !== false);
        },
 
@@ -163,8 +163,8 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * @returns {string}
         * The validation error at this time
         */
-       getValidationError: function() {
-               return this.validationError || '';
+       getValidationError() {
+               return this.validationError ?? '';
        },
 
        /**
@@ -177,11 +177,11 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * @instance
         * @memberof LuCI.ui.AbstractElement
         */
-       triggerValidation: function() {
+       triggerValidation() {
                if (typeof(this.vfunc) != 'function')
                        return false;
 
-               var wasValid = this.isValid();
+               const wasValid = this.isValid();
 
                this.vfunc();
 
@@ -212,12 +212,12 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * @param {string[]} events
         * The native DOM events for which event handlers should be registered.
         */
-       registerEvents: function(targetNode, synevent, events) {
-               var dispatchFn = L.bind(function(ev) {
+       registerEvents(targetNode, synevent, events) {
+               const dispatchFn = L.bind(function(ev) {
                        this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
                }, this);
 
-               for (var i = 0; i < events.length; i++)
+               for (let i = 0; i < events.length; i++)
                        targetNode.addEventListener(events[i], dispatchFn);
        },
 
@@ -237,19 +237,18 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * @param {...string} events
         * The DOM events for which event handlers should be registered.
         */
-       setUpdateEvents: function(targetNode /*, ... */) {
-               var datatype = this.options.datatype,
-                   optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
-                   validate = this.options.validate,
-                   events = this.varargs(arguments, 1);
+       setUpdateEvents(targetNode, ...events) {
+               const datatype = this.options.datatype;
+               const optional = this.options.hasOwnProperty('optional') ? this.options.optional : true;
+               const validate = this.options.validate;
 
                this.registerEvents(targetNode, 'widget-update', events);
 
                if (!datatype && !validate)
                        return;
 
-               this.vfunc = UI.prototype.addValidator.apply(UI.prototype, [
-                       targetNode, datatype || 'string',
+               this.vfunc = UI.prototype.addValidator(...[
+                       targetNode, datatype ?? 'string',
                        optional, validate
                ].concat(events));
 
@@ -282,13 +281,13 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * @param {...string} events
         * The DOM events for which event handlers should be registered.
         */
-       setChangeEvents: function(targetNode /*, ... */) {
-               var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
+       setChangeEvents(targetNode, ...events) {
+               const tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
 
-               for (var i = 1; i < arguments.length; i++)
-                       targetNode.addEventListener(arguments[i], tag_changed);
+               for (let i = 0; i < events.length; i++)
+                       targetNode.addEventListener(events[i], tag_changed);
 
-               this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
+               this.registerEvents(targetNode, 'widget-change', events);
        },
 
        /**
@@ -301,7 +300,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
         * Returns a DOM Node or DocumentFragment containing the rendered
         * widget markup.
         */
-       render: function() {}
+       render() {}
 });
 
 /**
@@ -330,7 +329,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
  * @param {LuCI.ui.Textfield.InitOptions} [options]
  * Object describing the widget specific options to initialize the input.
  */
-var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
+const UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -354,7 +353,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
         * Specifies the HTML `placeholder` attribute which is displayed when the
         * corresponding `<input>` element is empty.
         */
-       __init__: function(value, options) {
+       __init__(value, options) {
                this.value = value;
                this.options = Object.assign({
                        optional: true,
@@ -363,10 +362,10 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
        },
 
        /** @override */
-       render: function() {
-               var frameEl = E('div', { 'id': this.options.id });
-               var inputEl = E('input', {
-                       'id': this.options.id ? 'widget.' + this.options.id : null,
+       render() {
+               const frameEl = E('div', { 'id': this.options.id });
+               const inputEl = E('input', {
+                       'id': this.options.id ? `widget.${this.options.id}` : null,
                        'name': this.options.name,
                        'type': 'text',
                        'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
@@ -374,7 +373,6 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
                        'disabled': this.options.disabled ? '' : null,
                        'maxlength': this.options.maxlength,
                        'placeholder': this.options.placeholder,
-                       'autocomplete': this.options.password ? 'new-password' : null,
                        'value': this.value,
                });
 
@@ -386,14 +384,14 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
                                        'title': _('Reveal/hide password'),
                                        'aria-label': _('Reveal/hide password'),
                                        'click': function(ev) {
-                                               var e = this.previousElementSibling;
+                                               const e = this.previousElementSibling;
                                                e.type = (e.type === 'password') ? 'text' : 'password';
                                                ev.preventDefault();
                                        }
                                }, '∗')
                        ]));
 
-                       window.requestAnimationFrame(function() { inputEl.type = 'password' });
+                       window.requestAnimationFrame(() => { inputEl.type = 'password' });
                }
                else {
                        frameEl.appendChild(inputEl);
@@ -403,8 +401,8 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
        },
 
        /** @private */
-       bind: function(frameEl) {
-               var inputEl = frameEl.querySelector('input');
+       bind(frameEl) {
+               const inputEl = frameEl.querySelector('input');
 
                this.node = frameEl;
 
@@ -417,14 +415,14 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
        },
 
        /** @override */
-       getValue: function() {
-               var inputEl = this.node.querySelector('input');
+       getValue() {
+               const inputEl = this.node.querySelector('input');
                return inputEl.value;
        },
 
        /** @override */
-       setValue: function(value) {
-               var inputEl = this.node.querySelector('input');
+       setValue(value) {
+               const inputEl = this.node.querySelector('input');
                inputEl.value = value;
        }
 });
@@ -455,7 +453,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
  * @param {LuCI.ui.Textarea.InitOptions} [options]
  * Object describing the widget specific options to initialize the input.
  */
-var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
+const UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -485,7 +483,7 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
         * @property {boolean} [wrap=false]
         * Specifies whether the HTML `wrap` attribute should be set.
         */
-       __init__: function(value, options) {
+       __init__(value, options) {
                this.value = value;
                this.options = Object.assign({
                        optional: true,
@@ -496,13 +494,13 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
        },
 
        /** @override */
-       render: function() {
-               var style = !this.options.cols ? 'width:100%' : null,
-                   frameEl = E('div', { 'id': this.options.id, 'style': style }),
-                   value = (this.value != null) ? String(this.value) : '';
+       render() {
+               const style = !this.options.cols ? 'width:100%' : null;
+               const frameEl = E('div', { 'id': this.options.id, 'style': style });
+               const value = (this.value != null) ? String(this.value) : '';
 
                frameEl.appendChild(E('textarea', {
-                       'id': this.options.id ? 'widget.' + this.options.id : null,
+                       'id': this.options.id ? `widget.${this.options.id}` : null,
                        'name': this.options.name,
                        'class': 'cbi-input-textarea',
                        'readonly': this.options.readonly ? '' : null,
@@ -521,8 +519,8 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
        },
 
        /** @private */
-       bind: function(frameEl) {
-               var inputEl = frameEl.firstElementChild;
+       bind(frameEl) {
+               const inputEl = frameEl.firstElementChild;
 
                this.node = frameEl;
 
@@ -535,12 +533,12 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
        },
 
        /** @override */
-       getValue: function() {
+       getValue() {
                return this.node.firstElementChild.value;
        },
 
        /** @override */
-       setValue: function(value) {
+       setValue(value) {
                this.node.firstElementChild.value = value;
        }
 });
@@ -571,7 +569,7 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
  * @param {LuCI.ui.Checkbox.InitOptions} [options]
  * Object describing the widget specific options to initialize the input.
  */
-var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
+const UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -590,7 +588,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
         * checkbox. This is a legacy property existing for compatibility reasons,
         * it is required for HTML based form submissions.
         */
-       __init__: function(value, options) {
+       __init__(value, options) {
                this.value = value;
                this.options = Object.assign({
                        value_enabled: '1',
@@ -599,9 +597,9 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
        },
 
        /** @override */
-       render: function() {
-               var id = 'cb%08x'.format(Math.random() * 0xffffffff);
-               var frameEl = E('div', {
+       render() {
+               const id = 'cb%08x'.format(Math.random() * 0xffffffff);
+               const frameEl = E('div', {
                        'id': this.options.id,
                        'class': 'cbi-checkbox'
                });
@@ -620,13 +618,13 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
                        'value': this.options.value_enabled,
                        'checked': (this.value == this.options.value_enabled) ? '' : null,
                        'disabled': this.options.disabled ? '' : null,
-                       'data-widget-id': this.options.id ? 'widget.' + this.options.id : null
+                       'data-widget-id': this.options.id ? `widget.${this.options.id}` : null
                }));
 
                frameEl.appendChild(E('label', { 'for': id }));
 
                if (this.options.tooltip != null) {
-                       var icon = "⚠️";
+                       let icon = "⚠️";
 
                        if (this.options.tooltipicon != null)
                                icon = this.options.tooltipicon;
@@ -645,10 +643,10 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
        },
 
        /** @private */
-       bind: function(frameEl) {
+       bind(frameEl) {
                this.node = frameEl;
 
-               var input = frameEl.querySelector('input[type="checkbox"]');
+               const input = frameEl.querySelector('input[type="checkbox"]');
                this.setUpdateEvents(input, 'click', 'blur');
                this.setChangeEvents(input, 'change');
 
@@ -665,19 +663,19 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
         * @returns {boolean}
         * Returns `true` when the checkbox is currently checked, otherwise `false`.
         */
-       isChecked: function() {
+       isChecked() {
                return this.node.querySelector('input[type="checkbox"]').checked;
        },
 
        /** @override */
-       getValue: function() {
+       getValue() {
                return this.isChecked()
                        ? this.options.value_enabled
                        : this.options.value_disabled;
        },
 
        /** @override */
-       setValue: function(value) {
+       setValue(value) {
                this.node.querySelector('input[type="checkbox"]').checked = (value == this.options.value_enabled);
        }
 });
@@ -715,7 +713,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
  * @param {LuCI.ui.Select.InitOptions} [options]
  * Object describing the widget specific options to initialize the inputs.
  */
-var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
+const UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -750,7 +748,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
         * Specifies a placeholder text which is displayed when no choice is
         * selected yet. Only applicable to the `select` widget type.
         */
-       __init__: function(value, choices, options) {
+       __init__(value, choices, options) {
                if (!L.isObject(choices))
                        choices = {};
 
@@ -773,9 +771,9 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
        },
 
        /** @override */
-       render: function() {
-               var frameEl = E('div', { 'id': this.options.id }),
-                   keys = Object.keys(this.choices);
+       render() {
+               const frameEl = E('div', { 'id': this.options.id });
+               let keys = Object.keys(this.choices);
 
                if (this.options.sort === true)
                        keys.sort(L.naturalCompare);
@@ -784,7 +782,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
 
                if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
                        frameEl.appendChild(E('select', {
-                               'id': this.options.id ? 'widget.' + this.options.id : null,
+                               'id': this.options.id ? `widget.${this.options.id}` : null,
                                'name': this.options.name,
                                'size': this.options.size,
                                'class': 'cbi-input-select',
@@ -796,28 +794,28 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
                                frameEl.lastChild.appendChild(E('option', {
                                        'value': '',
                                        'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
-                               }, [ this.choices[''] || this.options.placeholder || _('-- Please choose --') ]));
+                               }, [ this.choices[''] ?? this.options.placeholder ?? _('-- Please choose --') ]));
 
-                       for (var i = 0; i < keys.length; i++) {
+                       for (let i = 0; i < keys.length; i++) {
                                if (keys[i] == null || keys[i] == '')
                                        continue;
 
                                frameEl.lastChild.appendChild(E('option', {
                                        'value': keys[i],
                                        'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
-                               }, [ this.choices[keys[i]] || keys[i] ]));
+                               }, [ this.choices[keys[i]] ?? keys[i] ]));
                        }
                }
                else {
-                       var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' \xa0 ') : E('br');
+                       const brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' \xa0 ') : E('br');
 
-                       for (var i = 0; i < keys.length; i++) {
+                       for (let i = 0; i < keys.length; i++) {
                                frameEl.appendChild(E('span', {
                                        'class': 'cbi-%s'.format(this.options.multiple ? 'checkbox' : 'radio')
                                }, [
                                        E('input', {
                                                'id': this.options.id ? 'widget.%s.%d'.format(this.options.id, i) : null,
-                                               'name': this.options.id || this.options.name,
+                                               'name': this.options.id ?? this.options.name,
                                                'type': this.options.multiple ? 'checkbox' : 'radio',
                                                'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
                                                'value': keys[i],
@@ -829,7 +827,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
                                                'click': function(ev) {
                                                        ev.currentTarget.previousElementSibling.previousElementSibling.click();
                                                }
-                                       }, [ this.choices[keys[i]] || keys[i] ])
+                                       }, [ this.choices[keys[i]] ?? keys[i] ])
                                ]));
 
                                frameEl.appendChild(brEl.cloneNode());
@@ -840,7 +838,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
        },
 
        /** @private */
-       bind: function(frameEl) {
+       bind(frameEl) {
                this.node = frameEl;
 
                if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
@@ -848,8 +846,8 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
                        this.setChangeEvents(frameEl.firstChild, 'change');
                }
                else {
-                       var radioEls = frameEl.querySelectorAll('input[type="radio"]');
-                       for (var i = 0; i < radioEls.length; i++) {
+                       const radioEls = frameEl.querySelectorAll('input[type="radio"]');
+                       for (let i = 0; i < radioEls.length; i++) {
                                this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
                                this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
                        }
@@ -861,12 +859,12 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
        },
 
        /** @override */
-       getValue: function() {
+       getValue() {
                if (this.options.widget != 'radio' && this.options.widget != 'checkbox')
                        return this.node.firstChild.value;
 
-               var radioEls = this.node.querySelectorAll('input[type="radio"]');
-               for (var i = 0; i < radioEls.length; i++)
+               const radioEls = this.node.querySelectorAll('input[type="radio"]');
+               for (let i = 0; i < radioEls.length; i++)
                        if (radioEls[i].checked)
                                return radioEls[i].value;
 
@@ -874,19 +872,19 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
        },
 
        /** @override */
-       setValue: function(value) {
+       setValue(value) {
                if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
                        if (value == null)
                                value = '';
 
-                       for (var i = 0; i < this.node.firstChild.options.length; i++)
+                       for (let i = 0; i < this.node.firstChild.options.length; i++)
                                this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
 
                        return;
                }
 
-               var radioEls = frameEl.querySelectorAll('input[type="radio"]');
-               for (var i = 0; i < radioEls.length; i++)
+               const radioEls = frameEl.querySelectorAll('input[type="radio"]');
+               for (let i = 0; i < radioEls.length; i++)
                        radioEls[i].checked = (radioEls[i].value == value);
        }
 });
@@ -923,7 +921,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
  * @param {LuCI.ui.Dropdown.InitOptions} [options]
  * Object describing the widget specific options to initialize the dropdown.
  */
-var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
+const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -1020,7 +1018,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
         * compatibility reasons. It is usually better to `maxlength(N)` validation
         * expression. Only applicable when `create` is `true`.
         */
-       __init__: function(value, choices, options) {
+       __init__(value, choices, options) {
                if (typeof(choices) != 'object')
                        choices = {};
 
@@ -1045,8 +1043,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @override */
-       render: function() {
-               var sb = E('div', {
+       render() {
+               const sb = E('div', {
                        'id': this.options.id,
                        'class': 'cbi-dropdown',
                        'multiple': this.options.multiple ? '' : null,
@@ -1055,7 +1053,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        'tabindex': -1
                }, E('ul'));
 
-               var keys = Object.keys(this.choices);
+               let keys = Object.keys(this.choices);
 
                if (this.options.sort === true)
                        keys.sort(L.naturalCompare);
@@ -1063,12 +1061,12 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        keys = this.options.sort;
 
                if (this.options.create)
-                       for (var i = 0; i < this.values.length; i++)
+                       for (let i = 0; i < this.values.length; i++)
                                if (!this.choices.hasOwnProperty(this.values[i]))
                                        keys.push(this.values[i]);
 
-               for (var i = 0; i < keys.length; i++) {
-                       var label = this.choices[keys[i]];
+               for (let i = 0; i < keys.length; i++) {
+                       let label = this.choices[keys[i]];
 
                        if (dom.elem(label))
                                label = label.cloneNode(true);
@@ -1076,20 +1074,20 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        sb.lastElementChild.appendChild(E('li', {
                                'data-value': keys[i],
                                'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
-                       }, [ label || keys[i] ]));
+                       }, [ label ?? keys[i] ]));
                }
 
                if (this.options.create) {
-                       var createEl = E('input', {
+                       const createEl = E('input', {
                                'type': 'text',
                                'class': 'create-item-input',
                                'readonly': this.options.readonly ? '' : null,
                                'maxlength': this.options.maxlength,
-                               'placeholder': this.options.custom_placeholder || this.options.placeholder
+                               'placeholder': this.options.custom_placeholder ?? this.options.placeholder
                        });
 
                        if (this.options.datatype || this.options.validate)
-                               UI.prototype.addValidator(createEl, this.options.datatype || 'string',
+                               UI.prototype.addValidator(createEl, this.options.datatype ?? 'string',
                                                          true, this.options.validate, 'blur', 'keyup');
 
                        sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
@@ -1103,29 +1101,29 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       bind: function(sb) {
-               var o = this.options;
+       bind(sb) {
+               const o = this.options;
 
                o.multiple = sb.hasAttribute('multiple');
                o.optional = sb.hasAttribute('optional');
-               o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
-               o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
-               o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
-               o.create_query = sb.getAttribute('item-create') || o.create_query;
-               o.create_template = sb.getAttribute('item-template') || o.create_template;
-
-               var ul = sb.querySelector('ul'),
-                   more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
-                   open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
-                   canary = sb.appendChild(E('div')),
-                   create = sb.querySelector(this.options.create_query),
-                   ndisplay = this.options.display_items,
-                   n = 0;
+               o.placeholder = sb.getAttribute('placeholder') ?? o.placeholder;
+               o.display_items = parseInt(sb.getAttribute('display-items') ?? o.display_items);
+               o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') ?? o.dropdown_items);
+               o.create_query = sb.getAttribute('item-create') ?? o.create_query;
+               o.create_template = sb.getAttribute('item-template') ?? o.create_template;
+
+               const ul = sb.querySelector('ul');
+               const more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···'));
+               const open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾'));
+               const canary = sb.appendChild(E('div'));
+               const create = sb.querySelector(this.options.create_query);
+               let ndisplay = this.options.display_items;
+               let n = 0;
 
                if (this.options.multiple) {
-                       var items = ul.querySelectorAll('li');
+                       let items = ul.querySelectorAll('li');
 
-                       for (var i = 0; i < items.length; i++) {
+                       for (let i = 0; i < items.length; i++) {
                                this.transformItem(sb, items[i]);
 
                                if (items[i].hasAttribute('selected') && ndisplay-- > 0)
@@ -1134,22 +1132,22 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                }
                else {
                        if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
-                               var placeholder = E('li', { placeholder: '' },
-                                       this.options.select_placeholder || this.options.placeholder);
+                               const placeholder = E('li', { placeholder: '' },
+                                       this.options.select_placeholder ?? this.options.placeholder);
 
                                ul.firstChild
                                        ? ul.insertBefore(placeholder, ul.firstChild)
                                        : ul.appendChild(placeholder);
                        }
 
-                       var items = ul.querySelectorAll('li'),
-                           sel = sb.querySelectorAll('[selected]');
+                       let items = ul.querySelectorAll('li');
+                       const sel = sb.querySelectorAll('[selected]');
 
-                       sel.forEach(function(s) {
+                       sel.forEach(s => {
                                s.removeAttribute('selected');
                        });
 
-                       var s = sel[0] || items[0];
+                       const s = sel[0] ?? items[0];
                        if (s) {
                                s.setAttribute('selected', '');
                                s.setAttribute('display', n++);
@@ -1174,7 +1172,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        sb.removeAttribute('empty');
 
                dom.content(more, (ndisplay == this.options.display_items)
-                       ? (this.options.select_placeholder || this.options.placeholder) : '···');
+                       ? (this.options.select_placeholder ?? this.options.placeholder) : '···');
 
 
                sb.addEventListener('click', this.handleClick.bind(this));
@@ -1183,7 +1181,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
 
                if ('ontouchstart' in window) {
-                       sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
+                       sb.addEventListener('touchstart', ev => ev.stopPropagation());
                        window.addEventListener('touchstart', this.closeAllDropdowns);
                }
                else {
@@ -1201,7 +1199,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        create.addEventListener('focus', this.handleCreateFocus.bind(this));
                        create.addEventListener('blur', this.handleCreateBlur.bind(this));
 
-                       var li = findParent(create, 'li');
+                       const li = findParent(create, 'li');
 
                        li.setAttribute('unselectable', '');
                        li.addEventListener('click', this.handleCreateClick.bind(this));
@@ -1218,10 +1216,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       getScrollParent: function(element) {
-               var parent = element,
-                   style = getComputedStyle(element),
-                   excludeStaticParent = (style.position === 'absolute');
+       getScrollParent(element) {
+               let parent = element;
+               let style = getComputedStyle(element);
+               const excludeStaticParent = (style.position === 'absolute');
 
                if (style.position === 'fixed')
                        return document.body;
@@ -1240,49 +1238,49 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       openDropdown: function(sb) {
-               var st = window.getComputedStyle(sb, null),
-                   ul = sb.querySelector('ul'),
-                   li = ul.querySelectorAll('li'),
-                   fl = findParent(sb, '.cbi-value-field'),
-                   sel = ul.querySelector('[selected]'),
-                   rect = sb.getBoundingClientRect(),
-                   items = Math.min(this.options.dropdown_items, li.length),
-                   scrollParent = this.getScrollParent(sb);
-
-               document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+       openDropdown(sb) {
+               const st = window.getComputedStyle(sb, null);
+               const ul = sb.querySelector('ul');
+               const li = ul.querySelectorAll('li');
+               const fl = findParent(sb, '.cbi-value-field');
+               const sel = ul.querySelector('[selected]');
+               const rect = sb.getBoundingClientRect();
+               const items = Math.min(this.options.dropdown_items, li.length);
+               const scrollParent = this.getScrollParent(sb);
+
+               document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
                        s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
                });
 
                sb.setAttribute('open', '');
 
-               var pv = ul.cloneNode(true);
-                   pv.classList.add('preview');
+               const pv = ul.cloneNode(true);
+               pv.classList.add('preview');
 
                if (fl)
                        fl.classList.add('cbi-dropdown-open');
 
                if ('ontouchstart' in window) {
-                       var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
-                           vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
-                           start = null;
-
-                       ul.style.top = sb.offsetHeight + 'px';
-                       ul.style.left = -rect.left + 'px';
-                       ul.style.right = (rect.right - vpWidth) + 'px';
-                       ul.style.maxHeight = (vpHeight * 0.5) + 'px';
+                       const vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth ?? 0);
+                       const vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight ?? 0);
+                       let start = null;
+
+                       ul.style.top = `${sb.offsetHeight}px`;
+                       ul.style.left = `${-rect.left}px`;
+                       ul.style.right = `${rect.right - vpWidth}px`;
+                       ul.style.maxHeight = `${vpHeight * 0.5}px`;
                        ul.style.WebkitOverflowScrolling = 'touch';
 
-                       var scrollFrom = scrollParent.scrollTop,
-                           scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
+                       const scrollFrom = scrollParent.scrollTop;
+                       const scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
 
-                       var scrollStep = function(timestamp) {
+                       const scrollStep = timestamp => {
                                if (!start) {
                                        start = timestamp;
                                        ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
                                }
 
-                               var duration = Math.max(timestamp - start, 1);
+                               const duration = Math.max(timestamp - start, 1);
                                if (duration < 100) {
                                        scrollParent.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
                                        window.requestAnimationFrame(scrollStep);
@@ -1298,72 +1296,72 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        ul.style.maxHeight = '1px';
                        ul.style.top = ul.style.bottom = '';
 
-                       window.requestAnimationFrame(function() {
-                               var containerRect = scrollParent.getBoundingClientRect(),
-                                   itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
-                                   fullHeight = 0,
-                                   spaceAbove = rect.top - containerRect.top,
-                                   spaceBelow = containerRect.bottom - rect.bottom;
+                       window.requestAnimationFrame(() => {
+                               const containerRect = scrollParent.getBoundingClientRect();
+                               const itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height;
+                               let fullHeight = 0;
+                               const spaceAbove = rect.top - containerRect.top;
+                               const spaceBelow = containerRect.bottom - rect.bottom;
 
-                               for (var i = 0; i < (items == -1 ? li.length : items); i++)
+                               for (let i = 0; i < (items == -1 ? li.length : items); i++)
                                        fullHeight += li[i].getBoundingClientRect().height;
 
                                if (fullHeight <= spaceBelow) {
-                                       ul.style.top = rect.height + 'px';
-                                       ul.style.maxHeight = spaceBelow + 'px';
+                                       ul.style.top = `${rect.height}px`;
+                                       ul.style.maxHeight = `${spaceBelow}px`;
                                }
                                else if (fullHeight <= spaceAbove) {
-                                       ul.style.bottom = rect.height + 'px';
-                                       ul.style.maxHeight = spaceAbove + 'px';
+                                       ul.style.bottom = `${rect.height}px`;
+                                       ul.style.maxHeight = `${spaceAbove}px`;
                                }
                                else if (spaceBelow >= spaceAbove) {
-                                       ul.style.top = rect.height + 'px';
-                                       ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
+                                       ul.style.top = `${rect.height}px`;
+                                       ul.style.maxHeight = `${spaceBelow - (spaceBelow % itemHeight)}px`;
                                }
                                else {
-                                       ul.style.bottom = rect.height + 'px';
-                                       ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
+                                       ul.style.bottom = `${rect.height}px`;
+                                       ul.style.maxHeight = `${spaceAbove - (spaceAbove % itemHeight)}px`;
                                }
 
                                ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
                        });
                }
 
-               var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
-               for (var i = 0; i < cboxes.length; i++) {
+               const cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
+               for (let i = 0; i < cboxes.length; i++) {
                        cboxes[i].checked = true;
                        cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
-               };
+               }
 
                ul.classList.add('dropdown');
 
                sb.insertBefore(pv, ul.nextElementSibling);
 
-               li.forEach(function(l) {
+               li.forEach(l => {
                        l.setAttribute('tabindex', 0);
                });
 
                sb.lastElementChild.setAttribute('tabindex', 0);
 
-               var focusFn = L.bind(function(el) {
+               const focusFn = L.bind(function(el) {
                        this.setFocus(sb, el, true);
                        ul.removeEventListener('transitionend', focusFn);
-               }, this, sel || li[0]);
+               }, this, sel ?? li[0]);
 
                ul.addEventListener('transitionend', focusFn);
        },
 
        /** @private */
-       closeDropdown: function(sb, no_focus) {
+       closeDropdown(sb, no_focus) {
                if (!sb.hasAttribute('open'))
                        return;
 
-               var pv = sb.querySelector('ul.preview'),
-                   ul = sb.querySelector('ul.dropdown'),
-                   li = ul.querySelectorAll('li'),
-                   fl = findParent(sb, '.cbi-value-field');
+               const pv = sb.querySelector('ul.preview');
+               const ul = sb.querySelector('ul.dropdown');
+               const li = ul.querySelectorAll('li');
+               const fl = findParent(sb, '.cbi-value-field');
 
-               li.forEach(function(l) { l.removeAttribute('tabindex'); });
+               li.forEach(l => l.removeAttribute('tabindex'));
                sb.lastElementChild.removeAttribute('tabindex');
 
                sb.removeChild(pv);
@@ -1383,20 +1381,20 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       toggleItem: function(sb, li, force_state) {
-               var ul = li.parentNode;
+       toggleItem(sb, li, force_state) {
+               const ul = li.parentNode;
 
                if (li.hasAttribute('unselectable'))
                        return;
 
                if (this.options.multiple) {
-                       var cbox = li.querySelector('input[type="checkbox"]'),
-                           items = li.parentNode.querySelectorAll('li'),
-                           label = sb.querySelector('ul.preview'),
-                           sel = li.parentNode.querySelectorAll('[selected]').length,
-                           more = sb.querySelector('.more'),
-                           ndisplay = this.options.display_items,
-                           n = 0;
+                       const cbox = li.querySelector('input[type="checkbox"]');
+                       const items = li.parentNode.querySelectorAll('li');
+                       const label = sb.querySelector('ul.preview');
+                       let sel = li.parentNode.querySelectorAll('[selected]').length;
+                       const more = sb.querySelector('.more');
+                       let ndisplay = this.options.display_items;
+                       let n = 0;
 
                        if (li.hasAttribute('selected')) {
                                if (force_state !== true) {
@@ -1422,7 +1420,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        while (label && label.firstElementChild)
                                label.removeChild(label.firstElementChild);
 
-                       for (var i = 0; i < items.length; i++) {
+                       for (let i = 0; i < items.length; i++) {
                                items[i].removeAttribute('display');
                                if (items[i].hasAttribute('selected')) {
                                        if (ndisplay-- > 0) {
@@ -1430,7 +1428,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                                                if (label)
                                                        label.appendChild(items[i].cloneNode(true));
                                        }
-                                       var c = items[i].querySelector('input[type="checkbox"]');
+                                       const c = items[i].querySelector('input[type="checkbox"]');
                                        if (c)
                                                c.disabled = (sel == 1 && !this.options.optional);
                                }
@@ -1447,10 +1445,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                                sb.removeAttribute('empty');
 
                        dom.content(more, (ndisplay === this.options.display_items)
-                               ? (this.options.select_placeholder || this.options.placeholder) : '···');
+                               ? (this.options.select_placeholder ?? this.options.placeholder) : '···');
                }
                else {
-                       var sel = li.parentNode.querySelector('[selected]');
+                       let sel = li.parentNode.querySelector('[selected]');
                        if (sel) {
                                sel.removeAttribute('display');
                                sel.removeAttribute('selected');
@@ -1466,9 +1464,9 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       transformItem: function(sb, li) {
-               var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
-                   label = E('label');
+       transformItem(sb, li) {
+               const cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' }));
+               const label = E('label');
 
                while (li.firstChild)
                        label.appendChild(li.firstChild);
@@ -1478,21 +1476,21 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       saveValues: function(sb, ul) {
-               var sel = ul.querySelectorAll('li[selected]'),
-                   div = sb.lastElementChild,
-                   name = this.options.name,
-                   strval = '',
-                   values = [];
+       saveValues(sb, ul) {
+               const sel = ul.querySelectorAll('li[selected]');
+               const div = sb.lastElementChild;
+               const name = this.options.name;
+               let strval = '';
+               const values = [];
 
                while (div.lastElementChild)
                        div.removeChild(div.lastElementChild);
 
-               sel.forEach(function (s) {
+               sel.forEach(s => {
                        if (s.hasAttribute('placeholder'))
                                return;
 
-                       var v = {
+                       const v = {
                                text: s.innerText,
                                value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
                                element: s
@@ -1506,10 +1504,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
 
                        values.push(v);
 
-                       strval += strval.length ? ' ' + v.value : v.value;
+                       strval += strval.length ? ` ${v.value}` : v.value;
                });
 
-               var detail = {
+               const detail = {
                        instance: this,
                        element: sb
                };
@@ -1528,11 +1526,11 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       setValues: function(sb, values) {
-               var ul = sb.querySelector('ul');
+       setValues(sb, values) {
+               const ul = sb.querySelector('ul');
 
                if (this.options.create) {
-                       for (var value in values) {
+                       for (const value in values) {
                                this.createItems(sb, value);
 
                                if (!this.options.multiple)
@@ -1541,9 +1539,9 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                }
 
                if (this.options.multiple) {
-                       var lis = ul.querySelectorAll('li[data-value]');
-                       for (var i = 0; i < lis.length; i++) {
-                               var value = lis[i].getAttribute('data-value');
+                       const lis = ul.querySelectorAll('li[data-value]');
+                       for (let i = 0; i < lis.length; i++) {
+                               const value = lis[i].getAttribute('data-value');
                                if (values === null || !(value in values))
                                        this.toggleItem(sb, lis[i], false);
                                else
@@ -1551,13 +1549,13 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        }
                }
                else {
-                       var ph = ul.querySelector('li[placeholder]');
+                       const ph = ul.querySelector('li[placeholder]');
                        if (ph)
                                this.toggleItem(sb, ph);
 
-                       var lis = ul.querySelectorAll('li[data-value]');
-                       for (var i = 0; i < lis.length; i++) {
-                               var value = lis[i].getAttribute('data-value');
+                       const lis = ul.querySelectorAll('li[data-value]');
+                       for (let i = 0; i < lis.length; i++) {
+                               const value = lis[i].getAttribute('data-value');
                                if (values !== null && (value in values))
                                        this.toggleItem(sb, lis[i]);
                        }
@@ -1565,11 +1563,11 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       setFocus: function(sb, elem, scroll) {
+       setFocus(sb, elem, scroll) {
                if (sb.hasAttribute('locked-in'))
                        return;
 
-               sb.querySelectorAll('.focus').forEach(function(e) {
+               sb.querySelectorAll('.focus').forEach(e => {
                        e.classList.remove('focus');
                });
 
@@ -1582,13 +1580,13 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleMouseout: function(ev) {
-               var sb = ev.currentTarget;
+       handleMouseout(ev) {
+               const sb = ev.currentTarget;
 
                if (!sb.hasAttribute('open'))
                        return;
 
-               sb.querySelectorAll('.focus').forEach(function(e) {
+               sb.querySelectorAll('.focus').forEach(e => {
                        e.classList.remove('focus');
                });
 
@@ -1596,20 +1594,20 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       createChoiceElement: function(sb, value, label) {
-               var tpl = sb.querySelector(this.options.create_template),
-                   markup = null;
+       createChoiceElement(sb, value, label) {
+               const tpl = sb.querySelector(this.options.create_template);
+               let markup = null;
 
                if (tpl)
-                       markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
+                       markup = (tpl.textContent ?? tpl.innerHTML ?? tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
                else
                        markup = '<li data-value="{{value}}"><span data-label-placeholder="true" /></li>';
 
-               var new_item = E(markup.replace(/{{value}}/g, '%h'.format(value))),
-                   placeholder = new_item.querySelector('[data-label-placeholder]');
+               const new_item = E(markup.replace(/{{value}}/g, '%h'.format(value)));
+               const placeholder = new_item.querySelector('[data-label-placeholder]');
 
                if (placeholder) {
-                       var content = E('span', {}, label || this.choices[value] || [ value ]);
+                       const content = E('span', {}, label ?? this.choices[value] ?? [ value ]);
 
                        while (content.firstChild)
                                placeholder.parentNode.insertBefore(content.firstChild, placeholder);
@@ -1624,20 +1622,20 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       createItems: function(sb, value) {
-               var sbox = this,
-                   val = (value || '').trim(),
-                   ul = sb.querySelector('ul');
+       createItems(sb, value) {
+               const sbox = this;
+               let val = (value ?? '').trim();
+               const ul = sb.querySelector('ul');
 
                if (!sbox.options.multiple)
                        val = val.length ? [ val ] : [];
                else
                        val = val.length ? val.split(/\s+/) : [];
 
-               val.forEach(function(item) {
-                       var new_item = null;
+               val.forEach(item => {
+                       let new_item = null;
 
-                       ul.childNodes.forEach(function(li) {
+                       ul.childNodes.forEach(li => {
                                if (li.getAttribute && li.getAttribute('data-value') === item)
                                        new_item = li;
                        });
@@ -1646,7 +1644,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                                new_item = sbox.createChoiceElement(sb, item);
 
                                if (!sbox.options.multiple) {
-                                       var old = ul.querySelector('li[created]');
+                                       const old = ul.querySelector('li[created]');
                                        if (old)
                                                ul.removeChild(old);
 
@@ -1674,14 +1672,14 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
         * If set to `true`, deselect and remove selected choices as well instead
         * of keeping them.
         */
-       clearChoices: function(reset_value) {
-               var ul = this.node.querySelector('ul'),
-                   lis = ul ? ul.querySelectorAll('li[data-value]') : [],
-                   len = lis.length - (this.options.create ? 1 : 0),
-                   val = reset_value ? null : this.getValue();
-
-               for (var i = 0; i < len; i++) {
-                       var lival = lis[i].getAttribute('data-value');
+       clearChoices(reset_value) {
+               const ul = this.node.querySelector('ul');
+               const lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+               const len = lis.length - (this.options.create ? 1 : 0);
+               const val = reset_value ? null : this.getValue();
+
+               for (let i = 0; i < len; i++) {
+                       const lival = lis[i].getAttribute('data-value');
                        if (val == null ||
                                (!this.options.multiple && val != lival) ||
                                (this.options.multiple && val.indexOf(lival) == -1))
@@ -1709,10 +1707,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
         * as label text. Choice labels may be any valid value accepted by
         * {@link LuCI.dom#content}.
         */
-       addChoices: function(values, labels) {
-               var sb = this.node,
-                   ul = sb.querySelector('ul'),
-                   lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+       addChoices(values, labels) {
+               const sb = this.node;
+               const ul = sb.querySelector('ul');
+               const lis = ul ? ul.querySelectorAll('li[data-value]') : [];
 
                if (!Array.isArray(values))
                        values = L.toArray(values);
@@ -1720,10 +1718,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                if (!L.isObject(labels))
                        labels = {};
 
-               for (var i = 0; i < values.length; i++) {
-                       var found = false;
+               for (let i = 0; i < values.length; i++) {
+                       let found = false;
 
-                       for (var j = 0; j < lis.length; j++) {
+                       for (let j = 0; j < lis.length; j++) {
                                if (lis[j].getAttribute('data-value') === values[i]) {
                                        found = true;
                                        break;
@@ -1742,22 +1740,22 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        /**
         * Close all open dropdown widgets in the current document.
         */
-       closeAllDropdowns: function() {
-               document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+       closeAllDropdowns() {
+               document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
                        s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
                });
        },
 
        /** @private */
-       handleClick: function(ev) {
-               var sb = ev.currentTarget;
+       handleClick(ev) {
+               const sb = ev.currentTarget;
 
                if (!sb.hasAttribute('open')) {
                        if (!matchesElem(ev.target, 'input'))
                                this.openDropdown(sb);
                }
                else {
-                       var li = findParent(ev.target, 'li');
+                       const li = findParent(ev.target, 'li');
                        if (li && li.parentNode.classList.contains('dropdown'))
                                this.toggleItem(sb, li);
                        else if (li && li.parentNode.classList.contains('preview'))
@@ -1771,9 +1769,9 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleKeydown: function(ev) {
-               var sb = ev.currentTarget,
-                   ul = sb.querySelector('ul.dropdown');
+       handleKeydown(ev) {
+               const sb = ev.currentTarget;
+               const ul = sb.querySelector('ul.dropdown');
 
                if (matchesElem(ev.target, 'input'))
                        return;
@@ -1789,7 +1787,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        }
                }
                else {
-                       var active = findParent(document.activeElement, 'li');
+                       const active = findParent(document.activeElement, 'li');
 
                        switch (ev.keyCode) {
                        case 27:
@@ -1839,16 +1837,16 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleDropdownClose: function(ev) {
-               var sb = ev.currentTarget;
+       handleDropdownClose(ev) {
+               const sb = ev.currentTarget;
 
                this.closeDropdown(sb, true);
        },
 
        /** @private */
-       handleDropdownSelect: function(ev) {
-               var sb = ev.currentTarget,
-                   li = findParent(ev.target, 'li');
+       handleDropdownSelect(ev) {
+               const sb = ev.currentTarget;
+               const li = findParent(ev.target, 'li');
 
                if (!li)
                        return;
@@ -1858,37 +1856,37 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleMouseover: function(ev) {
-               var sb = ev.currentTarget;
+       handleMouseover(ev) {
+               const sb = ev.currentTarget;
 
                if (!sb.hasAttribute('open'))
                        return;
 
-               var li = findParent(ev.target, 'li');
+               const li = findParent(ev.target, 'li');
 
                if (li && li.parentNode.classList.contains('dropdown'))
                        this.setFocus(sb, li);
        },
 
        /** @private */
-       handleFocus: function(ev) {
-               var sb = ev.currentTarget;
+       handleFocus(ev) {
+               const sb = ev.currentTarget;
 
-               document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+               document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
                        if (s !== sb || sb.hasAttribute('open'))
                                s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
                });
        },
 
        /** @private */
-       handleCanaryFocus: function(ev) {
+       handleCanaryFocus(ev) {
                this.closeDropdown(ev.currentTarget.parentNode);
        },
 
        /** @private */
-       handleCreateKeydown: function(ev) {
-               var input = ev.currentTarget,
-                   sb = findParent(input, '.cbi-dropdown');
+       handleCreateKeydown(ev) {
+               const input = ev.currentTarget;
+               const sb = findParent(input, '.cbi-dropdown');
 
                switch (ev.keyCode) {
                case 13:
@@ -1905,10 +1903,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleCreateFocus: function(ev) {
-               var input = ev.currentTarget,
-                   cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
-                   sb = findParent(input, '.cbi-dropdown');
+       handleCreateFocus(ev) {
+               const input = ev.currentTarget;
+               const cbox = findParent(input, 'li').querySelector('input[type="checkbox"]');
+               const sb = findParent(input, '.cbi-dropdown');
 
                if (cbox)
                        cbox.checked = true;
@@ -1917,10 +1915,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleCreateBlur: function(ev) {
-               var input = ev.currentTarget,
-                   cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
-                   sb = findParent(input, '.cbi-dropdown');
+       handleCreateBlur(ev) {
+               const input = ev.currentTarget;
+               const cbox = findParent(input, 'li').querySelector('input[type="checkbox"]');
+               const sb = findParent(input, '.cbi-dropdown');
 
                if (cbox)
                        cbox.checked = false;
@@ -1929,25 +1927,25 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @private */
-       handleCreateClick: function(ev) {
+       handleCreateClick(ev) {
                ev.currentTarget.querySelector(this.options.create_query).focus();
        },
 
        /** @override */
-       setValue: function(values) {
+       setValue(values) {
                if (this.options.multiple) {
                        if (!Array.isArray(values))
                                values = (values != null && values != '') ? [ values ] : [];
 
-                       var v = {};
+                       const v = {};
 
-                       for (var i = 0; i < values.length; i++)
+                       for (let i = 0; i < values.length; i++)
                                v[values[i]] = true;
 
                        this.setValues(this.node, v);
                }
                else {
-                       var v = {};
+                       const v = {};
 
                        if (values != null) {
                                if (Array.isArray(values))
@@ -1961,12 +1959,12 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        },
 
        /** @override */
-       getValue: function() {
-               var div = this.node.lastElementChild,
-                   h = div.querySelectorAll('input[type="hidden"]'),
-                       v = [];
+       getValue() {
+               const div = this.node.lastElementChild;
+               const h = div.querySelectorAll('input[type="hidden"]');
+               const v = [];
 
-               for (var i = 0; i < h.length; i++)
+               for (let i = 0; i < h.length; i++)
                        v.push(h[i].value);
 
                return this.options.multiple ? v : v[0];
@@ -2007,7 +2005,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
  * @param {LuCI.ui.Combobox.InitOptions} [options]
  * Object describing the widget specific options to initialize the dropdown.
  */
-var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
+const UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
        /**
         * Comboboxes support the same properties as
         * [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
@@ -2028,7 +2026,7 @@ var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
         * Since Comboboxes are always optional, this property is forcibly set to
         * `true`.
         */
-       __init__: function(value, choices, options) {
+       __init__(value, choices, options) {
                this.super('__init__', [ value, choices, Object.assign({
                        select_placeholder: _('-- Please choose --'),
                        custom_placeholder: _('-- custom --'),
@@ -2074,7 +2072,7 @@ var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
  * @param {LuCI.ui.ComboButton.InitOptions} [options]
  * Object describing the widget specific options to initialize the button.
  */
-var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype */ {
+const UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype */ {
        /**
         * ComboButtons support the same properties as
         * [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
@@ -2111,7 +2109,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
         * and receive the DOM click event as first as well as the selected action
         * choice value as second argument.
         */
-       __init__: function(value, choices, options) {
+       __init__(value, choices, options) {
                this.super('__init__', [ value, choices, Object.assign({
                        sort: true
                }, options, {
@@ -2122,35 +2120,35 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
        },
 
        /** @override */
-       render: function(/* ... */) {
-               var node = UIDropdown.prototype.render.apply(this, arguments),
-                   val = this.getValue();
+       render(...args) {
+               const node = UIDropdown.prototype.render.call(this, ...args);
+               const val = this.getValue();
 
                if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
-                       node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+                       node.setAttribute('class', `cbi-dropdown ${this.options.classes[val]}`);
 
                return node;
        },
 
        /** @private */
-       handleClick: function(ev) {
-               var sb = ev.currentTarget,
-                   t = ev.target;
+       handleClick(ev, ...args) {
+               const sb = ev.currentTarget;
+               const t = ev.target;
 
                if (sb.hasAttribute('open') || dom.matches(t, '.cbi-dropdown > span.open'))
-                       return UIDropdown.prototype.handleClick.apply(this, arguments);
+                       return UIDropdown.prototype.handleClick.call(this, ev, ...args);
 
                if (this.options.click)
                        return this.options.click.call(sb, ev, this.getValue());
        },
 
        /** @private */
-       toggleItem: function(sb /*, ... */) {
-               var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
-                   val = this.getValue();
+       toggleItem(sb, ...args) {
+               const rv = UIDropdown.prototype.toggleItem.call(this, sb, ...args);
+               const val = this.getValue();
 
                if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
-                       sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+                       sb.setAttribute('class', `cbi-dropdown ${this.options.classes[val]}`);
                else
                        sb.setAttribute('class', 'cbi-dropdown');
 
@@ -2193,7 +2191,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
  * @param {LuCI.ui.DynamicList.InitOptions} [options]
  * Object describing the widget specific options to initialize the dynamic list.
  */
-var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
+const UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
        /**
         * In case choices are passed to the dynamic list constructor, the widget
         * supports the same properties as [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions}
@@ -2211,7 +2209,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
         * predefined choice values, the dropdown must be made optional to allow
         * it to remain unselected.
         */
-       __init__: function(values, choices, options) {
+       __init__(values, choices, options) {
                if (!Array.isArray(values))
                        values = (values != null && values != '') ? [ values ] : [];
 
@@ -2227,8 +2225,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @override */
-       render: function() {
-               var dl = E('div', {
+       render() {
+               const dl = E('div', {
                        'id': this.options.id,
                        'class': 'cbi-dynlist',
                        'disabled': this.options.disabled ? '' : null
@@ -2238,13 +2236,13 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                        if (this.options.placeholder != null)
                                this.options.select_placeholder = this.options.placeholder;
 
-                       var cbox = new UICombobox(null, this.choices, this.options);
+                       const cbox = new UICombobox(null, this.choices, this.options);
 
                        dl.lastElementChild.appendChild(cbox.render());
                }
                else {
-                       var inputEl = E('input', {
-                               'id': this.options.id ? 'widget.' + this.options.id : null,
+                       const inputEl = E('input', {
+                               'id': this.options.id ? `widget.${this.options.id}` : null,
                                'type': 'text',
                                'class': 'cbi-input-text',
                                'placeholder': this.options.placeholder,
@@ -2255,12 +2253,12 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                        dl.lastElementChild.appendChild(E('div', { 'class': 'btn cbi-button cbi-button-add' }, '+'));
 
                        if (this.options.datatype || this.options.validate)
-                               UI.prototype.addValidator(inputEl, this.options.datatype || 'string',
+                               UI.prototype.addValidator(inputEl, this.options.datatype ?? 'string',
                                                          true, this.options.validate, 'blur', 'keyup');
                }
 
-               for (var i = 0; i < this.values.length; i++) {
-                       var label = this.choices ? this.choices[this.values[i]] : null;
+               for (let i = 0; i < this.values.length; i++) {
+                       let label = this.choices ? this.choices[this.values[i]] : null;
 
                        if (dom.elem(label))
                                label = label.cloneNode(true);
@@ -2272,7 +2270,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       bind: function(dl) {
+       bind(dl) {
                dl.addEventListener('click', L.bind(this.handleClick, this));
                dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
                dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
@@ -2288,20 +2286,21 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       addItem: function(dl, value, text, flash) {
-               var exists = false,
-                   new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
-                               E('span', {}, [ text || value ]),
-                               E('input', {
-                                       'type': 'hidden',
-                                       'name': this.options.name,
-                                       'value': value })]);
+       addItem(dl, value, text, flash) {
+               let exists = false;
+
+               const new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
+                       E('span', {}, [ text ?? value ]),
+                       E('input', {
+                               'type': 'hidden',
+                               'name': this.options.name,
+                               'value': value })]);
 
-               dl.querySelectorAll('.item').forEach(function(item) {
+               dl.querySelectorAll('.item').forEach(item => {
                        if (exists)
                                return;
 
-                       var hidden = item.querySelector('input[type="hidden"]');
+                       let hidden = item.querySelector('input[type="hidden"]');
 
                        if (hidden && hidden.parentNode !== item)
                                hidden = null;
@@ -2311,7 +2310,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                });
 
                if (!exists) {
-                       var ai = dl.querySelector('.add-item');
+                       const ai = dl.querySelector('.add-item');
                        ai.parentNode.insertBefore(new_item, ai);
                }
 
@@ -2327,11 +2326,11 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       removeItem: function(dl, item) {
-               var value = item.querySelector('input[type="hidden"]').value;
-               var sb = dl.querySelector('.cbi-dropdown');
+       removeItem(dl, item) {
+               const value = item.querySelector('input[type="hidden"]').value;
+               const sb = dl.querySelector('.cbi-dropdown');
                if (sb)
-                       sb.querySelectorAll('ul > li').forEach(function(li) {
+                       sb.querySelectorAll('ul > li').forEach(li => {
                                if (li.getAttribute('data-value') === value) {
                                        if (li.hasAttribute('dynlistcustom'))
                                                li.parentNode.removeChild(li);
@@ -2354,9 +2353,9 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       handleClick: function(ev) {
-               var dl = ev.currentTarget,
-                   item = findParent(ev.target, '.item');
+       handleClick(ev) {
+               const dl = ev.currentTarget;
+               const item = findParent(ev.target, '.item');
 
                if (this.options.disabled)
                        return;
@@ -2365,7 +2364,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                        this.removeItem(dl, item);
                }
                else if (matchesElem(ev.target, '.cbi-button-add')) {
-                       var input = ev.target.previousElementSibling;
+                       const input = ev.target.previousElementSibling;
                        if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
                                this.addItem(dl, input.value, null, true);
                                input.value = '';
@@ -2374,11 +2373,11 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       handleDropdownChange: function(ev) {
-               var dl = ev.currentTarget,
-                   sbIn = ev.detail.instance,
-                   sbEl = ev.detail.element,
-                   sbVal = ev.detail.value;
+       handleDropdownChange(ev) {
+               const dl = ev.currentTarget;
+               const sbIn = ev.detail.instance;
+               const sbEl = ev.detail.element;
+               const sbVal = ev.detail.value;
 
                if (sbVal === null)
                        return;
@@ -2391,12 +2390,12 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                        sbVal.element.setAttribute('dynlistcustom', '');
                }
 
-               var label = sbVal.text;
+               let label = sbVal.text;
 
                if (sbVal.element) {
                        label = E([]);
 
-                       for (var i = 0; i < sbVal.element.childNodes.length; i++)
+                       for (let i = 0; i < sbVal.element.childNodes.length; i++)
                                label.appendChild(sbVal.element.childNodes[i].cloneNode(true));
                }
 
@@ -2404,9 +2403,9 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @private */
-       handleKeydown: function(ev) {
-               var dl = ev.currentTarget,
-                   item = findParent(ev.target, '.item');
+       handleKeydown(ev) {
+               const dl = ev.currentTarget;
+               const item = findParent(ev.target, '.item');
 
                if (item) {
                        switch (ev.keyCode) {
@@ -2446,34 +2445,34 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
        },
 
        /** @override */
-       getValue: function() {
-               var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
-                   input = this.node.querySelector('.add-item > input[type="text"]'),
-                   v = [];
+       getValue() {
+               const items = this.node.querySelectorAll('.item > input[type="hidden"]');
+               const input = this.node.querySelector('.add-item > input[type="text"]');
+               const v = [];
 
-               for (var i = 0; i < items.length; i++)
+               for (let i = 0; i < items.length; i++)
                        v.push(items[i].value);
 
                if (input && input.value != null && input.value.match(/\S/) &&
                    input.classList.contains('cbi-input-invalid') == false &&
-                   v.filter(function(s) { return s == input.value }).length == 0)
+                   v.filter(s => s == input.value).length == 0)
                        v.push(input.value);
 
                return v;
        },
 
        /** @override */
-       setValue: function(values) {
+       setValue(values) {
                if (!Array.isArray(values))
                        values = (values != null && values != '') ? [ values ] : [];
 
-               var items = this.node.querySelectorAll('.item');
+               const items = this.node.querySelectorAll('.item');
 
-               for (var i = 0; i < items.length; i++)
+               for (let i = 0; i < items.length; i++)
                        if (items[i].parentNode === this.node)
                                this.removeItem(this.node, items[i]);
 
-               for (var i = 0; i < values.length; i++)
+               for (let i = 0; i < values.length; i++)
                        this.addItem(this.node, values[i],
                                this.choices ? this.choices[values[i]] : null);
        },
@@ -2495,8 +2494,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
         * as label text. Choice labels may be any valid value accepted by
         * {@link LuCI.dom#content}.
         */
-       addChoices: function(values, labels) {
-               var dl = this.node.lastElementChild.firstElementChild;
+       addChoices(values, labels) {
+               const dl = this.node.lastElementChild.firstElementChild;
                dom.callClassMethod(dl, 'addChoices', values, labels);
        },
 
@@ -2508,8 +2507,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
         * @instance
         * @memberof LuCI.ui.DynamicList
         */
-       clearChoices: function() {
-               var dl = this.node.lastElementChild.firstElementChild;
+       clearChoices() {
+               const dl = this.node.lastElementChild.firstElementChild;
                dom.callClassMethod(dl, 'clearChoices');
        }
 });
@@ -2541,8 +2540,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
  * @param {LuCI.ui.AbstractElement.InitOptions} [options]
  * Object describing the widget specific options to initialize the hidden input.
  */
-var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */ {
-       __init__: function(value, options) {
+const UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */ {
+       __init__(value, options) {
                this.value = value;
                this.options = Object.assign({
 
@@ -2550,8 +2549,8 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
        },
 
        /** @override */
-       render: function() {
-               var hiddenEl = E('input', {
+       render() {
+               const hiddenEl = E('input', {
                        'id': this.options.id,
                        'type': 'hidden',
                        'value': this.value
@@ -2561,7 +2560,7 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
        },
 
        /** @private */
-       bind: function(hiddenEl) {
+       bind(hiddenEl) {
                this.node = hiddenEl;
 
                dom.bindClassInstance(hiddenEl, this);
@@ -2570,12 +2569,12 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
        },
 
        /** @override */
-       getValue: function() {
+       getValue() {
                return this.node.value;
        },
 
        /** @override */
-       setValue: function(value) {
+       setValue(value) {
                this.node.value = value;
        }
 });
@@ -2608,7 +2607,7 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
  * Object describing the widget specific options to initialize the file
  * upload control.
  */
-var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
+const UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        /**
         * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
         * the following properties are recognized:
@@ -2643,7 +2642,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
         * Whether remote directories are browsable or not solely depends on the
         * ACL setup for the current session.
         */
-       __init__: function(value, options) {
+       __init__(value, options) {
                this.value = value;
                this.options = Object.assign({
                        show_hidden: false,
@@ -2654,7 +2653,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       bind: function(browserEl) {
+       bind(browserEl) {
                this.node = browserEl;
 
                this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
@@ -2666,9 +2665,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @override */
-       render: function() {
+       render() {
                return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
-                       var label;
+                       let label;
 
                        if (L.isObject(stat) && stat.type != 'directory')
                                this.stat = stat;
@@ -2699,15 +2698,15 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       truncatePath: function(path) {
+       truncatePath(path) {
                if (path.length > 50)
-                       path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+                       path = `${path.substring(0, 25)}…${path.substring(path.length - 25)}`;
 
                return path;
        },
 
        /** @private */
-       iconForType: function(type) {
+       iconForType(type) {
                switch (type) {
                case 'symlink':
                        return E('img', {
@@ -2736,7 +2735,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       canonicalizePath: function(path) {
+       canonicalizePath(path) {
                return path.replace(/\/{2,}/, '/')
                        .replace(/\/\.(\/|$)/g, '/')
                        .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
@@ -2744,9 +2743,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       splitPath: function(path) {
-               var croot = this.canonicalizePath(this.options.root_directory || '/'),
-                   cpath = this.canonicalizePath(path || '/');
+       splitPath(path) {
+               const croot = this.canonicalizePath(this.options.root_directory ?? '/');
+               const cpath = this.canonicalizePath(path ?? '/');
 
                if (cpath.length <= croot.length)
                        return [ croot ];
@@ -2754,7 +2753,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                if (cpath.charAt(croot.length) != '/')
                        return [ croot ];
 
-               var parts = cpath.substring(croot.length + 1).split(/\//);
+               const parts = cpath.substring(croot.length + 1).split(/\//);
 
                parts.unshift(croot);
 
@@ -2762,36 +2761,36 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleUpload: function(path, list, ev) {
-               var form = ev.target.parentNode,
-                   fileinput = form.querySelector('input[type="file"]'),
-                   nameinput = form.querySelector('input[type="text"]'),
-                   filename = (nameinput.value != null ? nameinput.value : '').trim();
+       handleUpload(path, list, ev) {
+               const form = ev.target.parentNode;
+               const fileinput = form.querySelector('input[type="file"]');
+               const nameinput = form.querySelector('input[type="text"]');
+               const filename = (nameinput.value != null ? nameinput.value : '').trim();
 
                ev.preventDefault();
 
                if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
                        return;
 
-               var existing = list.filter(function(e) { return e.name == filename })[0];
+               const existing = list.filter(e => e.name == filename)[0];
 
                if (existing != null && existing.type == 'directory')
                        return alert(_('A directory with the same name already exists.'));
                else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
                        return;
 
-               var data = new FormData();
+               const data = new FormData();
 
                data.append('sessionid', L.env.sessionid);
-               data.append('filename', path + '/' + filename);
+               data.append('filename', `${path}/${filename}`);
                data.append('filedata', fileinput.files[0]);
 
-               return request.post(L.env.cgi_base + '/cgi-upload', data, {
-                       progress: L.bind(function(btn, ev) {
+               return request.post(`${L.env.cgi_base}/cgi-upload`, data, {
+                       progress: L.bind((btn, ev) => {
                                btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
                        }, this, ev.target)
                }).then(L.bind(function(path, ev, res) {
-                       var reply = res.json();
+                       const reply = res.json();
 
                        if (L.isObject(reply) && reply.failure)
                                alert(_('Upload request failed: %s').format(reply.message));
@@ -2801,10 +2800,10 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleDelete: function(path, fileStat, ev) {
-               var parent = path.replace(/\/[^\/]+$/, '') || '/',
-                   name = path.replace(/^.+\//, ''),
-                   msg;
+       handleDelete(path, fileStat, ev) {
+               const parent = path.replace(/\/[^\/]+$/, '') ?? '/';
+               const name = path.replace(/^.+\//, '');
+               let msg;
 
                ev.preventDefault();
 
@@ -2814,8 +2813,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                        msg = _('Do you really want to delete "%s" ?').format(name);
 
                if (confirm(msg)) {
-                       var button = this.node.firstElementChild,
-                           hidden = this.node.lastElementChild;
+                       const button = this.node.firstElementChild;
+                       const hidden = this.node.lastElementChild;
 
                        if (path == hidden.value) {
                                dom.content(button, _('Select file…'));
@@ -2824,14 +2823,14 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
 
                        return fs.remove(path).then(L.bind(function(parent, ev) {
                                return this.handleSelect(parent, null, ev);
-                       }, this, parent, ev)).catch(function(err) {
+                       }, this, parent, ev)).catch(err => {
                                alert(_('Delete request failed: %s').format(err.message));
                        });
                }
        },
 
        /** @private */
-       renderUpload: function(path, list) {
+       renderUpload(path, list) {
                if (!this.options.enable_upload)
                        return E([]);
 
@@ -2840,8 +2839,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                'href': '#',
                                'class': 'btn cbi-button-positive',
                                'click': function(ev) {
-                                       var uploadForm = ev.target.nextElementSibling,
-                                           fileInput = uploadForm.querySelector('input[type="file"]');
+                                       const uploadForm = ev.target.nextElementSibling;
+                                       const fileInput = uploadForm.querySelector('input[type="file"]');
 
                                        ev.target.style.display = 'none';
                                        uploadForm.style.display = '';
@@ -2853,8 +2852,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                        'type': 'file',
                                        'style': 'display:none',
                                        'change': function(ev) {
-                                               var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
-                                                   uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
+                                               const nameinput = ev.target.parentNode.querySelector('input[type="text"]');
+                                               const uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
 
                                                nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
                                                uploadbtn.disabled = false;
@@ -2878,22 +2877,22 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       renderListing: function(container, path, list) {
-               var breadcrumb = E('p'),
-                   rows = E('ul');
+       renderListing(container, path, list) {
+               const breadcrumb = E('p');
+               const rows = E('ul');
 
-               list.sort(function(a, b) {
+               list.sort((a, b) => {
                        return L.naturalCompare(a.type == 'directory', b.type == 'directory') ||
-                              L.naturalCompare(a.name, b.name);
+                                  L.naturalCompare(a.name, b.name);
                });
 
-               for (var i = 0; i < list.length; i++) {
+               for (let i = 0; i < list.length; i++) {
                        if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
                                continue;
 
-                       var entrypath = this.canonicalizePath(path + '/' + list[i].name),
-                           selected = (entrypath == this.node.lastElementChild.value),
-                           mtime = new Date(list[i].mtime * 1000);
+                       const entrypath = this.canonicalizePath(`${path}/${list[i].name}`);
+                       const selected = (entrypath == this.node.lastElementChild.value);
+                       const mtime = new Date(list[i].mtime * 1000);
 
                        rows.appendChild(E('li', [
                                E('div', { 'class': 'name' }, [
@@ -2931,16 +2930,16 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                if (!rows.firstElementChild)
                        rows.appendChild(E('em', _('No entries in this directory')));
 
-               var dirs = this.splitPath(path),
-                   cur = '';
+               const dirs = this.splitPath(path);
+               let cur = '';
 
-               for (var i = 0; i < dirs.length; i++) {
-                       cur = cur ? cur + '/' + dirs[i] : dirs[i];
+               for (let i = 0; i < dirs.length; i++) {
+                       cur = cur ? `${cur}/${dirs[i]}` : dirs[i];
                        dom.append(breadcrumb, [
                                i ? ' » ' : '',
                                E('a', {
                                        'href': '#',
-                                       'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur || '/', null)
+                                       'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur ?? '/', null)
                                }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
                        ]);
                }
@@ -2960,9 +2959,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleCancel: function(ev) {
-               var button = this.node.firstElementChild,
-                   browser = button.nextElementSibling;
+       handleCancel(ev) {
+               const button = this.node.firstElementChild;
+               const browser = button.nextElementSibling;
 
                browser.classList.remove('open');
                button.style.display = '';
@@ -2973,9 +2972,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleReset: function(ev) {
-               var button = this.node.firstElementChild,
-                   hidden = this.node.lastElementChild;
+       handleReset(ev) {
+               const button = this.node.firstElementChild;
+               const hidden = this.node.lastElementChild;
 
                hidden.value = '';
                dom.content(button, _('Select file…'));
@@ -2984,17 +2983,17 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleSelect: function(path, fileStat, ev) {
-               var browser = dom.parent(ev.target, '.cbi-filebrowser'),
-                   ul = browser.querySelector('ul');
+       handleSelect(path, fileStat, ev) {
+               const browser = dom.parent(ev.target, '.cbi-filebrowser');
+               const ul = browser.querySelector('ul');
 
                if (fileStat == null) {
                        dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
                        L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
                }
                else {
-                       var button = this.node.firstElementChild,
-                           hidden = this.node.lastElementChild;
+                       const button = this.node.firstElementChild;
+                       const hidden = this.node.lastElementChild;
 
                        path = this.canonicalizePath(path);
 
@@ -3013,10 +3012,10 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @private */
-       handleFileBrowser: function(ev) {
-               var button = ev.target,
-                   browser = button.nextElementSibling,
-                   path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : (this.options.initial_directory || this.options.root_directory);
+       handleFileBrowser(ev) {
+               const button = ev.target;
+               const browser = button.nextElementSibling;
+               let path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : (this.options.initial_directory ?? this.options.root_directory);
 
                if (path.indexOf(this.options.root_directory) != 0)
                        path = this.options.root_directory;
@@ -3024,7 +3023,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                ev.preventDefault();
 
                return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
-                       document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
+                       document.querySelectorAll('.cbi-filebrowser.open').forEach(browserEl => {
                                dom.findClassInstance(browserEl).handleCancel(ev);
                        });
 
@@ -3036,36 +3035,36 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
        },
 
        /** @override */
-       getValue: function() {
+       getValue() {
                return this.node.lastElementChild.value;
        },
 
        /** @override */
-       setValue: function(value) {
+       setValue(value) {
                this.node.lastElementChild.value = value;
        }
 });
 
 
 function scrubMenu(node) {
-       var hasSatisfiedChild = false;
+       let hasSatisfiedChild = false;
 
        if (L.isObject(node.children)) {
-               for (var k in node.children) {
-                       var child = scrubMenu(node.children[k]);
+               for (const k in node.children) {
+                       const child = scrubMenu(node.children[k]);
 
                        if (child.title && !child.firstchild_ineligible)
-                               hasSatisfiedChild = hasSatisfiedChild || child.satisfied;
+                               hasSatisfiedChild ||= child.satisfied;
                }
        }
 
        if (L.isObject(node.action) &&
-           node.action.type == 'firstchild' &&
-           hasSatisfiedChild == false)
+               node.action.type == 'firstchild' &&
+               hasSatisfiedChild == false)
                node.satisfied = false;
 
        return node;
-};
+}
 
 /**
  * Handle menu.
@@ -3077,7 +3076,7 @@ function scrubMenu(node) {
  *
  * Handles menus.
  */
-var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
+const UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
        /**
         * @typedef {Object} MenuNode
         * @memberof LuCI.ui.menu
@@ -3096,7 +3095,7 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
         * @returns {Promise<LuCI.ui.menu.MenuNode>}
         * Returns a promise resolving to the root element of the menu tree.
         */
-       load: function() {
+       load() {
                if (this.menu == null)
                        this.menu = session.getLocalData('menu');
 
@@ -3116,7 +3115,7 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
         * Flush the internal menu cache to force loading a new structure on the
         * next page load.
         */
-       flushCache: function() {
+       flushCache() {
                session.setLocalData('menu', null);
        },
 
@@ -3128,13 +3127,13 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
         * @returns {LuCI.ui.menu.MenuNode[]}
         * Returns an array of child menu nodes.
         */
-       getChildren: function(node) {
-               var children = [];
+       getChildren(node) {
+               const children = [];
 
                if (node == null)
                        node = this.menu;
 
-               for (var k in node.children) {
+               for (const k in node.children) {
                        if (!node.children.hasOwnProperty(k))
                                continue;
 
@@ -3144,14 +3143,14 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
                        if (!node.children[k].hasOwnProperty('title'))
                                continue;
 
-                       var subnode = Object.assign(node.children[k], { name: k });
+                       let subnode = Object.assign(node.children[k], { name: k });
 
                        if (L.isObject(subnode.action) && subnode.action.path != null &&
-                           (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
-                               var root = this.menu,
-                                   path = subnode.action.path.split('/');
+                               (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
+                               let root = this.menu;
+                               const path = subnode.action.path.split('/');
 
-                               for (var i = 0; root != null && i < path.length; i++)
+                               for (let i = 0; root != null && i < path.length; i++)
                                        root = L.isObject(root.children) ? root.children[path[i]] : null;
 
                                if (root)
@@ -3164,9 +3163,9 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
                        children.push(subnode);
                }
 
-               return children.sort(function(a, b) {
-                       var wA = a.order || 1000,
-                           wB = b.order || 1000;
+               return children.sort((a, b) => {
+                       const wA = a.order ?? 1000;
+                       const wB = b.order ?? 1000;
 
                        if (wA != wB)
                                return wA - wB;
@@ -3176,17 +3175,17 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
        }
 });
 
-var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
-       __init__: function(captions, options, placeholder) {
+const UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
+       __init__(captions, options, placeholder) {
                if (!Array.isArray(captions)) {
                        this.initFromMarkup(captions);
 
                        return;
                }
 
-               var id = options.id || 'table%08x'.format(Math.random() * 0xffffffff);
+               const id = options.id ?? 'table%08x'.format(Math.random() * 0xffffffff);
 
-               var table = E('table', { 'id': id, 'class': 'table' }, [
+               const table = E('table', { 'id': id, 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles', 'click': UI.prototype.createHandlerFn(this, 'handleSort') })
                ]);
 
@@ -3194,13 +3193,13 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                this.node = table
                this.options = options;
 
-               var sorting = this.getActiveSortState();
+               const sorting = this.getActiveSortState();
 
-               for (var i = 0; i < captions.length; i++) {
+               for (let i = 0; i < captions.length; i++) {
                        if (captions[i] == null)
                                continue;
 
-                       var th = E('th', { 'class': 'th' }, [ captions[i] ]);
+                       const th = E('th', { 'class': 'th' }, [ captions[i] ]);
 
                        if (typeof(options.captionClasses) == 'object')
                                DOMTokenList.prototype.add.apply(th.classList, L.toArray(options.captionClasses[i]));
@@ -3216,8 +3215,8 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                }
 
                if (placeholder) {
-                       var trow = table.appendChild(E('tr', { 'class': 'tr placeholder' })),
-                           td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
+                       const trow = table.appendChild(E('tr', { 'class': 'tr placeholder' }));
+                       const td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
 
                        if (typeof(captionClasses) == 'object')
                                DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
@@ -3226,19 +3225,19 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                DOMTokenList.prototype.add.apply(table.classList, L.toArray(options.classes));
        },
 
-       update: function(data, placeholder) {
-               var placeholder = placeholder || this.options.placeholder || _('No data', 'empty table placeholder'),
-                   sorting = this.getActiveSortState();
+       update(data, placeholderText) {
+               const placeholder = placeholderText ?? this.options.placeholder ?? _('No data', 'empty table placeholder');
+               const sorting = this.getActiveSortState();
 
                if (!Array.isArray(data))
                        return;
 
                if (sorting) {
-                       var list = data.map(L.bind(function(row) {
+                       const list = data.map(L.bind(function(row) {
                                return [ this.deriveSortKey(row[sorting[0]], sorting[0]), row ];
                        }, this));
 
-                       list.sort(function(a, b) {
+                       list.sort((a, b) => {
                                return sorting[1]
                                        ? -L.naturalCompare(a[0], b[0])
                                        : L.naturalCompare(a[0], b[0]);
@@ -3246,7 +3245,7 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
 
                        data.length = 0;
 
-                       list.forEach(function(item) {
+                       list.forEach(item => {
                                data.push(item[1]);
                        });
                }
@@ -3254,22 +3253,22 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                this.data = data;
                this.placeholder = placeholder;
 
-               var n = 0,
-                   rows = this.node.querySelectorAll('tr, .tr'),
-                   trows = [],
-                   headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th, .th')),
-                   captionClasses = this.options.captionClasses,
-                   trTag = (rows[0] && rows[0].nodeName == 'DIV') ? 'div' : 'tr',
-                   tdTag = (headings[0] && headings[0].nodeName == 'DIV') ? 'div' : 'td';
+               let n = 0;
+               const rows = this.node.querySelectorAll('tr, .tr');
+               const trows = [];
+               const headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th, .th'));
+               const captionClasses = this.options.captionClasses;
+               const trTag = (rows[0] && rows[0].nodeName == 'DIV') ? 'div' : 'tr';
+               const tdTag = (headings[0] && headings[0].nodeName == 'DIV') ? 'div' : 'td';
 
-               data.forEach(function(row) {
+               data.forEach(row => {
                        trows[n] = E(trTag, { 'class': 'tr' });
 
-                       for (var i = 0; i < headings.length; i++) {
-                               var text = (headings[i].innerText || '').trim();
-                               var raw_val = Array.isArray(row[i]) ? row[i][0] : null;
-                               var disp_val = Array.isArray(row[i]) ? row[i][1] : row[i];
-                               var td = trows[n].appendChild(E(tdTag, {
+                       for (let i = 0; i < headings.length; i++) {
+                               const text = (headings[i].innerText ?? '').trim();
+                               const raw_val = Array.isArray(row[i]) ? row[i][0] : null;
+                               const disp_val = Array.isArray(row[i]) ? row[i][1] : row[i];
+                               const td = trows[n].appendChild(E(tdTag, {
                                        'class': 'td',
                                        'data-title': (text !== '') ? text : null,
                                        'data-value': raw_val
@@ -3285,7 +3284,7 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                        trows[n].classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
                });
 
-               for (var i = 0; i < n; i++) {
+               for (let i = 0; i < n; i++) {
                        if (rows[i+1])
                                this.node.replaceChild(trows[i], rows[i+1]);
                        else
@@ -3296,8 +3295,8 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                        this.node.removeChild(rows[n]);
 
                if (placeholder && this.node.firstElementChild === this.node.lastElementChild) {
-                       var trow = this.node.appendChild(E(trTag, { 'class': 'tr placeholder' })),
-                           td = trow.appendChild(E(tdTag, { 'class': 'td' }, placeholder));
+                       const trow = this.node.appendChild(E(trTag, { 'class': 'tr placeholder' }));
+                       const td = trow.appendChild(E(tdTag, { 'class': 'td' }, placeholder));
 
                        if (typeof(captionClasses) == 'object')
                                DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
@@ -3306,31 +3305,31 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                return this.node;
        },
 
-       render: function() {
+       render() {
                return this.node;
        },
 
        /** @private */
-       initFromMarkup: function(node) {
+       initFromMarkup(node) {
                if (!dom.elem(node))
                        node = document.querySelector(node);
 
                if (!node)
                        throw 'Invalid table selector';
 
-               var options = {},
-                   headrow = node.querySelector('tr, .tr');
+               const options = {};
+               const headrow = node.querySelector('tr, .tr');
 
                if (!headrow)
                        return;
 
-               options.classes = [].slice.call(node.classList).filter(function(c) { return c != 'table' });
+               options.classes = [].slice.call(node.classList).filter(c => c != 'table');
                options.sortable = [];
                options.captionClasses = [];
 
-               headrow.querySelectorAll('th, .th').forEach(function(th, i) {
+               headrow.querySelectorAll('th, .th').forEach((th, i) => {
                        options.sortable[i] = !th.classList.contains('cbi-section-actions');
-                       options.captionClasses[i] = [].slice.call(th.classList).filter(function(c) { return c != 'th' });
+                       options.captionClasses[i] = [].slice.call(th.classList).filter(c => c != 'th');
                });
 
                headrow.addEventListener('click', UI.prototype.createHandlerFn(this, 'handleSort'));
@@ -3341,9 +3340,10 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
        },
 
        /** @private */
-       deriveSortKey: function(value, index) {
-               var opts = this.options || {},
-                   hint, m;
+       deriveSortKey(value, index) {
+               const opts = this.options ?? {};
+               let hint;
+               let m;
 
                if (opts.sortable == true || opts.sortable == null)
                        hint = 'auto';
@@ -3354,16 +3354,17 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
                        if (value.hasAttribute('data-value'))
                                value = value.getAttribute('data-value');
                        else
-                               value = (value.innerText || '').trim();
+                               value = (value.innerText ?? '').trim();
                }
 
-               switch (hint || 'auto') {
+               switch (hint ?? 'auto') {
                case true:
                case 'auto':
                        m = /^([0-9a-fA-F:.]+)(?:\/([0-9a-fA-F:.]+))?$/.exec(value);
 
                        if (m) {
-                               var addr, mask;
+                               let addr;
+                               let mask;
 
                                addr = validation.parseIPv6(m[1]);
                                mask = m[2] ? validation.parseIPv6(m[2]) : null;
@@ -3418,13 +3419,13 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
        },
 
        /** @private */
-       getActiveSortState: function() {
+       getActiveSortState() {
                if (this.sortState)
                        return this.sortState;
 
-               var page = document.body.getAttribute('data-page'),
-                   key = page + '.' + this.id,
-                   state = session.getLocalData('tablesort');
+               const page = document.body.getAttribute('data-page');
+               const key = `${page}.${this.id}`;
+               const state = session.getLocalData('tablesort');
 
                if (L.isObject(state) && Array.isArray(state[key]))
                        return state[key];
@@ -3433,15 +3434,15 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
        },
 
        /** @private */
-       setActiveSortState: function(index, descending) {
+       setActiveSortState(index, descending) {
                this.sortState = [ index, descending ];
 
                if (!this.options.id)
                        return;
 
-               var page = document.body.getAttribute('data-page'),
-                   key = page + '.' + this.id,
-                   state = session.getLocalData('tablesort');
+               const page = document.body.getAttribute('data-page');
+               const key = `${page}.${this.id}`;
+               let state = session.getLocalData('tablesort');
 
                if (!L.isObject(state))
                        state = {};
@@ -3452,15 +3453,15 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
        },
 
        /** @private */
-       handleSort: function(ev) {
+       handleSort(ev) {
                if (!ev.target.matches('th[data-sortable-row]'))
                        return;
 
-               var th = ev.target,
-                   direction = (th.getAttribute('data-sort-direction') == 'asc'),
-                   index = 0;
+               const th = ev.target;
+               const direction = (th.getAttribute('data-sort-direction') == 'asc');
+               let index = 0;
 
-               this.node.firstElementChild.querySelectorAll('th').forEach(function(other_th, i) {
+               this.node.firstElementChild.querySelectorAll('th').forEach((other_th, i) => {
                        if (other_th !== th)
                                other_th.removeAttribute('data-sort-direction');
                        else
@@ -3484,8 +3485,8 @@ var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
  * To import the class in views, use `'require ui'`, to import it in
  * external JavaScript, use `L.require("ui").then(...)`.
  */
-var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
-       __init__: function() {
+const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
+       __init__() {
                modalDiv = document.body.appendChild(
                        dom.create('div', {
                                id: 'modal_overlay',
@@ -3548,13 +3549,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * @returns {Node}
         * Returns a DOM Node representing the modal dialog element.
         */
-       showModal: function(title, children /* , ... */) {
-               var dlg = modalDiv.firstElementChild;
+       showModal(title, children, ...classes) {
+               const dlg = modalDiv.firstElementChild;
 
                dlg.setAttribute('class', 'modal');
-
-               for (var i = 2; i < arguments.length; i++)
-                       dlg.classList.add(arguments[i]);
+               dlg.classList.add(...classes);
 
                dom.content(dlg, dom.create('h4', {}, title));
                dom.append(dlg, children);
@@ -3576,15 +3575,15 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * will not invoke other class functions so it suitable to be used as event
         * handler as-is without the need to bind it first.
         */
-       hideModal: function() {
+       hideModal() {
                document.body.classList.remove('modal-overlay-active');
                modalDiv.blur();
        },
 
        /** @private */
-       cancelModal: function(ev) {
+       cancelModal(ev) {
                if (ev.key == 'Escape') {
-                       var btn = modalDiv.querySelector('.right > button, .right > .btn');
+                       const btn = modalDiv.querySelector('.right > button, .right > .btn');
 
                        if (btn)
                                btn.click();
@@ -3592,8 +3591,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
        },
 
        /** @private */
-       showTooltip: function(ev) {
-               var target = findParent(ev.target, '[data-tooltip]');
+       showTooltip(ev) {
+               const target = findParent(ev.target, '[data-tooltip]');
 
                if (!target)
                        return;
@@ -3603,10 +3602,10 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                        tooltipTimeout = null;
                }
 
-               var rect = target.getBoundingClientRect(),
-                   x = rect.left              + window.pageXOffset,
-                   y = rect.top + rect.height + window.pageYOffset,
-                   above = false;
+               const rect = target.getBoundingClientRect();
+               const x = rect.left              + window.pageXOffset;
+               let y = rect.top + rect.height + window.pageYOffset;
+               let above = false;
 
                tooltipDiv.className = 'cbi-tooltip';
                tooltipDiv.innerHTML = '▲ ';
@@ -3618,18 +3617,18 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset))
                        above = true;
 
-               var dropdown = target.querySelector('ul.dropdown[style]:first-child');
+               const dropdown = target.querySelector('ul.dropdown[style]:first-child');
 
                if (dropdown && dropdown.style.top)
                        above = true;
 
                if (above) {
                        y -= (tooltipDiv.offsetHeight + target.offsetHeight);
-                       tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
+                       tooltipDiv.firstChild.data = `▼ ${tooltipDiv.firstChild.data.substr(2)}`;
                }
 
-               tooltipDiv.style.top = y + 'px';
-               tooltipDiv.style.left = x + 'px';
+               tooltipDiv.style.top = `${y}px`;
+               tooltipDiv.style.left = `${x}px`;
                tooltipDiv.style.opacity = 1;
 
                tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
@@ -3639,7 +3638,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
        },
 
        /** @private */
-       hideTooltip: function(ev) {
+       hideTooltip(ev) {
                if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
                    tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
                        return;
@@ -3650,7 +3649,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                }
 
                tooltipDiv.style.opacity = 0;
-               tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
+               tooltipTimeout = window.setTimeout(() => tooltipDiv.removeAttribute('style'), 250);
 
                tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
        },
@@ -3685,13 +3684,13 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * @returns {Node}
         * Returns a DOM Node representing the notification banner element.
         */
-       addNotification: function(title, children /*, ... */) {
-               var mc = document.querySelector('#maincontent') || document.body;
-               var msg = E('div', {
+       addNotification(title, children, ...classes) {
+               const mc = document.querySelector('#maincontent') ?? document.body;
+               const msg = E('div', {
                        'class': 'alert-message fade-in',
                        'style': 'display:flex',
                        'transitionend': function(ev) {
-                               var node = ev.currentTarget;
+                               const node = ev.currentTarget;
                                if (node.parentNode && node.classList.contains('fade-out'))
                                        node.parentNode.removeChild(node);
                        }
@@ -3714,8 +3713,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
 
                dom.append(msg.firstElementChild, children);
 
-               for (var i = 2; i < arguments.length; i++)
-                       msg.classList.add(arguments[i]);
+               msg.classList.add(...classes);
 
                mc.insertBefore(msg, mc.firstElementChild);
 
@@ -3757,7 +3755,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * Returns `true` when the indicator has been updated or `false` when no
         * changes were made.
         */
-       showIndicator: function(id, label, handler, style) {
+       showIndicator(id, label, handler, style) {
                if (indicatorDiv == null) {
                        indicatorDiv = document.body.querySelector('#indicators');
 
@@ -3765,11 +3763,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                return false;
                }
 
-               var handlerFn = (typeof(handler) == 'function') ? handler : null,
-                   indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id));
+               const handlerFn = (typeof(handler) == 'function') ? handler : null;
+               let indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id));
 
                if (indicatorElem == null) {
-                       var beforeElem = null;
+                       let beforeElem = null;
 
                        for (beforeElem = indicatorDiv.firstElementChild;
                             beforeElem != null;
@@ -3805,8 +3803,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * Returns `true` when the indicator has been removed or `false` when the
         * requested indicator was not found.
         */
-       hideIndicator: function(id) {
-               var indicatorElem = indicatorDiv ? indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) : null;
+       hideIndicator(id) {
+               const indicatorElem = indicatorDiv ? indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) : null;
 
                if (indicatorElem == null)
                        return false;
@@ -3848,19 +3846,19 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * @returns {Node}
         * Returns the parent DOM node the formatted markup has been added to.
         */
-       itemlist: function(node, items, separators) {
-               var children = [];
+       itemlist(node, items, separators) {
+               const children = [];
 
                if (!Array.isArray(separators))
-                       separators = [ separators || E('br') ];
+                       separators = [ separators ?? E('br') ];
 
-               for (var i = 0; i < items.length; i += 2) {
+               for (let i = 0; i < items.length; i += 2) {
                        if (items[i+1] !== null && items[i+1] !== undefined) {
-                               var sep = separators[(i/2) % separators.length],
-                                   cld = [];
+                               const sep = separators[(i/2) % separators.length];
+                               const cld = [];
 
                                children.push(E('span', { class: 'nowrap' }, [
-                                       items[i] ? E('strong', items[i] + ': ') : '',
+                                       items[i] ? E('strong', `${items[i]}: `) : '',
                                        items[i+1]
                                ]));
 
@@ -3891,11 +3889,13 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         */
        tabs: baseclass.singleton(/* @lends LuCI.ui.tabs.prototype */ {
                /** @private */
-               init: function() {
-                       var groups = [], prevGroup = null, currGroup = null;
+               init() {
+                       const groups = [];
+                       let prevGroup = null;
+                       let currGroup = null;
 
-                       document.querySelectorAll('[data-tab]').forEach(function(tab) {
-                               var parent = tab.parentNode;
+                       document.querySelectorAll('[data-tab]').forEach(tab => {
+                               const parent = tab.parentNode;
 
                                if (dom.matches(tab, 'li') && dom.matches(parent, 'ul.cbi-tabmenu'))
                                        return;
@@ -3915,7 +3915,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                groups[currGroup].push(tab);
                        });
 
-                       for (var i = 0; i < groups.length; i++)
+                       for (let i = 0; i < groups.length; i++)
                                this.initTabGroup(groups[i]);
 
                        document.addEventListener('dependency-update', this.updateTabs.bind(this));
@@ -3944,22 +3944,22 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * of a `querySelectorAll()` call or the `.childNodes` property of a
                 * DOM node.
                 */
-               initTabGroup: function(panes) {
+               initTabGroup(panes) {
                        if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
                                return;
 
-                       var menu = E('ul', { 'class': 'cbi-tabmenu' }),
-                           group = panes[0].parentNode,
-                           groupId = +group.getAttribute('data-tab-group'),
-                           selected = null;
+                       const menu = E('ul', { 'class': 'cbi-tabmenu' });
+                       const group = panes[0].parentNode;
+                       const groupId = +group.getAttribute('data-tab-group');
+                       let selected = null;
 
                        if (group.getAttribute('data-initialized') === 'true')
                                return;
 
-                       for (var i = 0, pane; pane = panes[i]; i++) {
-                               var name = pane.getAttribute('data-tab'),
-                                   title = pane.getAttribute('data-tab-title'),
-                                   active = pane.getAttribute('data-tab-active') === 'true';
+                       for (let i = 0, pane; pane = panes[i]; i++) {
+                               const name = pane.getAttribute('data-tab');
+                               const title = pane.getAttribute('data-tab-title');
+                               const active = pane.getAttribute('data-tab-active') === 'true';
 
                                menu.appendChild(E('li', {
                                        'style': this.isEmptyPane(pane) ? 'display:none' : null,
@@ -3981,7 +3981,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                selected = this.getActiveTabId(panes[0]);
 
                                if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
-                                       for (var i = 0; i < panes.length; i++) {
+                                       for (let i = 0; i < panes.length; i++) {
                                                if (!this.isEmptyPane(panes[i])) {
                                                        selected = i;
                                                        break;
@@ -3996,7 +3996,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                this.setActiveTabId(panes[selected], selected);
                        }
 
-                       requestAnimationFrame(L.bind(function(pane) {
+                       requestAnimationFrame(L.bind(pane => {
                                pane.dispatchEvent(new CustomEvent('cbi-tab-active', {
                                        detail: { tab: pane.getAttribute('data-tab') }
                                }));
@@ -4016,13 +4016,14 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * @returns {boolean}
                 * Returns `true` if the pane is empty, else `false`.
                 */
-               isEmptyPane: function(pane) {
-                       return dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
+               isEmptyPane(pane) {
+                       return dom.isEmpty(pane, n => n.classList.contains('cbi-tab-descr'));
                },
 
                /** @private */
-               getPathForPane: function(pane) {
-                       var path = [], node = null;
+               getPathForPane(pane) {
+                       const path = [];
+                       let node = null;
 
                        for (node = pane ? pane.parentNode : null;
                             node != null && node.hasAttribute != null;
@@ -4038,9 +4039,9 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               getActiveTabState: function() {
-                       var page = document.body.getAttribute('data-page'),
-                           state = session.getLocalData('tab');
+               getActiveTabState() {
+                       const page = document.body.getAttribute('data-page');
+                       const state = session.getLocalData('tab');
 
                        if (L.isObject(state) && state.page === page && L.isObject(state.paths))
                                return state;
@@ -4051,15 +4052,15 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               getActiveTabId: function(pane) {
-                       var path = this.getPathForPane(pane);
-                       return +this.getActiveTabState().paths[path] || 0;
+               getActiveTabId(pane) {
+                       const path = this.getPathForPane(pane);
+                       return +this.getActiveTabState().paths[path] ?? 0;
                },
 
                /** @private */
-               setActiveTabId: function(pane, tabIndex) {
-                       var path = this.getPathForPane(pane),
-                           state = this.getActiveTabState();
+               setActiveTabId(pane, tabIndex) {
+                       const path = this.getPathForPane(pane);
+                       const state = this.getActiveTabState();
 
                        state.paths[path] = tabIndex;
 
@@ -4067,11 +4068,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               updateTabs: function(ev, root) {
-                       (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
-                               var menu = pane.parentNode.previousElementSibling,
-                                   tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
-                                   n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
+               updateTabs(ev, root) {
+                       (root ?? document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
+                               const menu = pane.parentNode.previousElementSibling;
+                               const tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null;
+                               const n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
 
                                if (!menu || !tab)
                                        return;
@@ -4082,7 +4083,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                }
                                else if (tab.style.display === 'none') {
                                        tab.style.display = '';
-                                       requestAnimationFrame(function() { tab.classList.add('flash') });
+                                       requestAnimationFrame(() => tab.classList.add('flash'));
                                }
 
                                if (n_errors) {
@@ -4098,27 +4099,27 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               switchTab: function(ev) {
-                       var tab = ev.target.parentNode,
-                           name = tab.getAttribute('data-tab'),
-                           menu = tab.parentNode,
-                           group = menu.nextElementSibling,
-                           groupId = +group.getAttribute('data-tab-group'),
-                           index = 0;
+               switchTab(ev) {
+                       const tab = ev.target.parentNode;
+                       const name = tab.getAttribute('data-tab');
+                       const menu = tab.parentNode;
+                       const group = menu.nextElementSibling;
+                       const groupId = +group.getAttribute('data-tab-group');
+                       let index = 0;
 
                        ev.preventDefault();
 
                        if (!tab.classList.contains('cbi-tab-disabled'))
                                return;
 
-                       menu.querySelectorAll('[data-tab]').forEach(function(tab) {
+                       menu.querySelectorAll('[data-tab]').forEach(tab => {
                                tab.classList.remove('cbi-tab');
                                tab.classList.remove('cbi-tab-disabled');
                                tab.classList.add(
                                        tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
                        });
 
-                       group.childNodes.forEach(function(pane) {
+                       group.childNodes.forEach(pane => {
                                if (dom.matches(pane, '[data-tab]')) {
                                        if (pane.getAttribute('data-tab') === name) {
                                                pane.setAttribute('data-tab-active', 'true');
@@ -4163,8 +4164,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * or rejecting with an error in case the upload failed or has been
         * cancelled by the user.
         */
-       uploadFile: function(path, progressStatusNode) {
-               return new Promise(function(resolveFn, rejectFn) {
+       uploadFile(path, progressStatusNode) {
+               return new Promise((resolveFn, rejectFn) => {
                        UI.prototype.showModal(_('Uploading file…'), [
                                E('p', _('Please select the file to upload.')),
                                E('div', { 'style': 'display:flex' }, [
@@ -4172,11 +4173,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                E('input', {
                                                        type: 'file',
                                                        style: 'display:none',
-                                                       change: function(ev) {
-                                                               var modal = dom.parent(ev.target, '.modal'),
-                                                                   body = modal.querySelector('p'),
-                                                                   upload = modal.querySelector('.cbi-button-action.important'),
-                                                                   file = ev.currentTarget.files[0];
+                                                       change(ev) {
+                                                               const modal = dom.parent(ev.target, '.modal');
+                                                               const body = modal.querySelector('p');
+                                                               const upload = modal.querySelector('.cbi-button-action.important');
+                                                               const file = ev.currentTarget.files[0];
 
                                                                if (file == null)
                                                                        return;
@@ -4212,27 +4213,27 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                        'class': 'btn cbi-button-action important',
                                                        'disabled': true,
                                                        'click': function(ev) {
-                                                               var input = dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
+                                                               const input = dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
 
                                                                if (!input.files[0])
                                                                        return;
 
-                                                               var progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
+                                                               const progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
 
                                                                UI.prototype.showModal(_('Uploading file…'), [ progress ]);
 
-                                                               var data = new FormData();
+                                                               const data = new FormData();
 
                                                                data.append('sessionid', rpc.getSessionID());
                                                                data.append('filename', path);
                                                                data.append('filedata', input.files[0]);
 
-                                                               var filename = input.files[0].name;
+                                                               const filename = input.files[0].name;
 
-                                                               request.post(L.env.cgi_base + '/cgi-upload', data, {
+                                                               request.post(`${L.env.cgi_base}/cgi-upload`, data, {
                                                                        timeout: 0,
-                                                                       progress: function(pev) {
-                                                                               var percent = (pev.loaded / pev.total) * 100;
+                                                                       progress(pev) {
+                                                                               const percent = (pev.loaded / pev.total) * 100;
 
                                                                                if (progressStatusNode)
                                                                                        progressStatusNode.data = '%.2f%%'.format(percent);
@@ -4240,8 +4241,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                                                progress.setAttribute('title', '%.2f%%'.format(percent));
                                                                                progress.firstElementChild.style.width = '%.2f%%'.format(percent);
                                                                        }
-                                                               }).then(function(res) {
-                                                                       var reply = res.json();
+                                                               }).then(res => {
+                                                                       const reply = res.json();
 
                                                                        UI.prototype.hideModal();
 
@@ -4253,7 +4254,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                                                reply.name = filename;
                                                                                resolveFn(reply);
                                                                        }
-                                                               }, function(err) {
+                                                               }, err => {
                                                                        UI.prototype.hideModal();
                                                                        rejectFn(err);
                                                                });
@@ -4285,11 +4286,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * reachable or rejecting with an `error` event in case it is not reachable
         * or rejecting with `null` when the connectivity check timed out.
         */
-       pingDevice: function(proto, ipaddr) {
-               var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
+       pingDevice(proto, ipaddr) {
+               const target = '%s://%s%s?%s'.format(proto ?? 'http', ipaddr ?? window.location.host, L.resource('icons/loading.gif'), Math.random());
 
-               return new Promise(function(resolveFn, rejectFn) {
-                       var img = new Image();
+               return new Promise((resolveFn, rejectFn) => {
+                       const img = new Image();
 
                        img.onload = resolveFn;
                        img.onerror = rejectFn;
@@ -4311,19 +4312,20 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * If omitted, the current value of `window.location.host` is used by
         * default.
         */
-       awaitReconnect: function(/* ... */) {
-               var ipaddrs = arguments.length ? arguments : [ window.location.host ];
+       awaitReconnect(...hosts) {
+               const ipaddrs = hosts.length ? hosts : [ window.location.host ];
 
                window.setTimeout(L.bind(function() {
                        poll.add(L.bind(function() {
-                               var tasks = [], reachable = false;
+                               const tasks = [];
+                               let reachable = false;
 
-                               for (var i = 0; i < 2; i++)
-                                       for (var j = 0; j < ipaddrs.length; j++)
+                               for (let i = 0; i < 2; i++)
+                                       for (let j = 0; j < ipaddrs.length; j++)
                                                tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
-                                                       .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
+                                                       .then(ev => { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, () => {}));
 
-                               return Promise.all(tasks).then(function() {
+                               return Promise.all(tasks).then(() => {
                                        if (reachable) {
                                                poll.stop();
                                                window.location = reachable;
@@ -4348,7 +4350,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * `changes` property of the class instance value.
         */
        changes: baseclass.singleton(/* @lends LuCI.ui.changes.prototype */ {
-               init: function() {
+               init() {
                        if (!L.env.sessionid)
                                return;
 
@@ -4368,7 +4370,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * @param {number} n
                 * The number of changes to indicate.
                 */
-               setIndicator: function(n) {
+               setIndicator(n) {
                        if (n > 0) {
                                UI.prototype.showIndicator('uci-changes',
                                        '%s: %d'.format(_('Unsaved Changes'), n),
@@ -4390,10 +4392,10 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * @param {Object<string, Array<LuCI.uci.ChangeRecord>>} changes
                 * The UCI changeset to count.
                 */
-               renderChangeIndicator: function(changes) {
-                       var n_changes = 0;
+               renderChangeIndicator(changes) {
+                       let n_changes = 0;
 
-                       for (var config in changes)
+                       for (const config in changes)
                                if (changes.hasOwnProperty(config))
                                        n_changes += changes[config].length;
 
@@ -4424,60 +4426,61 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * @instance
                 * @memberof LuCI.ui.changes
                 */
-               displayChanges: function() {
-                       var list = E('div', { 'class': 'uci-change-list' }),
-                           dlg = UI.prototype.showModal(_('Configuration') + ' / ' + _('Changes'), [
-                               E('div', { 'class': 'cbi-section' }, [
-                                       E('strong', _('Legend:')),
-                                       E('div', { 'class': 'uci-change-legend' }, [
-                                               E('div', { 'class': 'uci-change-legend-label' }, [
-                                                       E('ins', '&#160;'), ' ', _('Section added') ]),
-                                               E('div', { 'class': 'uci-change-legend-label' }, [
-                                                       E('del', '&#160;'), ' ', _('Section removed') ]),
-                                               E('div', { 'class': 'uci-change-legend-label' }, [
-                                                       E('var', {}, E('ins', '&#160;')), ' ', _('Option changed') ]),
-                                               E('div', { 'class': 'uci-change-legend-label' }, [
-                                                       E('var', {}, E('del', '&#160;')), ' ', _('Option removed') ])]),
-                                       E('br'), list,
-                                       E('div', { 'class': 'right' }, [
-                                               E('button', {
-                                                       'class': 'btn',
-                                                       'click': UI.prototype.hideModal
-                                               }, [ _('Close') ]), ' ',
-                                               new UIComboButton('0', {
-                                                       0: [ _('Save & Apply') ],
-                                                       1: [ _('Apply unchecked') ]
-                                               }, {
-                                                       classes: {
-                                                               0: 'btn cbi-button cbi-button-positive important',
-                                                               1: 'btn cbi-button cbi-button-negative important'
-                                                       },
-                                                       click: L.bind(function(ev, mode) { this.apply(mode == '0') }, this)
-                                               }).render(), ' ',
-                                               E('button', {
-                                                       'class': 'cbi-button cbi-button-reset',
-                                                       'click': L.bind(this.revert, this)
-                                               }, [ _('Revert') ])])])
-                       ]);
+               displayChanges() {
+                       const list = E('div', { 'class': 'uci-change-list' });
+
+                       const dlg = UI.prototype.showModal(`${_('Configuration')} / ${_('Changes')}`, [
+                       E('div', { 'class': 'cbi-section' }, [
+                               E('strong', _('Legend:')),
+                               E('div', { 'class': 'uci-change-legend' }, [
+                                       E('div', { 'class': 'uci-change-legend-label' }, [
+                                               E('ins', '&#160;'), ' ', _('Section added') ]),
+                                       E('div', { 'class': 'uci-change-legend-label' }, [
+                                               E('del', '&#160;'), ' ', _('Section removed') ]),
+                                       E('div', { 'class': 'uci-change-legend-label' }, [
+                                               E('var', {}, E('ins', '&#160;')), ' ', _('Option changed') ]),
+                                       E('div', { 'class': 'uci-change-legend-label' }, [
+                                               E('var', {}, E('del', '&#160;')), ' ', _('Option removed') ])]),
+                               E('br'), list,
+                               E('div', { 'class': 'right' }, [
+                                       E('button', {
+                                               'class': 'btn',
+                                               'click': UI.prototype.hideModal
+                                       }, [ _('Close') ]), ' ',
+                                       new UIComboButton('0', {
+                                               0: [ _('Save & Apply') ],
+                                               1: [ _('Apply unchecked') ]
+                                       }, {
+                                               classes: {
+                                                       0: 'btn cbi-button cbi-button-positive important',
+                                                       1: 'btn cbi-button cbi-button-negative important'
+                                               },
+                                               click: L.bind(function(ev, mode) { this.apply(mode == '0') }, this)
+                                       }).render(), ' ',
+                                       E('button', {
+                                               'class': 'cbi-button cbi-button-reset',
+                                               'click': L.bind(this.revert, this)
+                                       }, [ _('Revert') ])])])
+               ]);
 
-                       for (var config in this.changes) {
+                       for (const config in this.changes) {
                                if (!this.changes.hasOwnProperty(config))
                                        continue;
 
                                list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
 
-                               for (var i = 0, added = null; i < this.changes[config].length; i++) {
-                                       var chg = this.changes[config][i],
-                                           tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
+                               for (let i = 0, added = null; i < this.changes[config].length; i++) {
+                                       const chg = this.changes[config][i];
+                                       const tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
 
-                                       list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
+                                       list.appendChild(E(tpl.replace(/%([01234])/g, (m0, m1) => {
                                                switch (+m1) {
                                                case 0:
                                                        return config;
 
                                                case 2:
                                                        if (added != null && chg[1] == added[0])
-                                                               return '@' + added[1] + '[-1]';
+                                                               return `@${added[1]}[-1]`;
                                                        else
                                                                return chg[1];
 
@@ -4499,9 +4502,9 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               displayStatus: function(type, content) {
+               displayStatus(type, content) {
                        if (type) {
-                               var message = UI.prototype.showModal('', '');
+                               const message = UI.prototype.showModal('', '');
 
                                message.classList.add('alert-message');
                                DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
@@ -4523,14 +4526,14 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               checkConnectivityAffected: function() {
+               checkConnectivityAffected() {
                        return L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr', null, 'json')).then(L.bind(function(info) {
                                if (L.isObject(info) && Array.isArray(info.inbound_interfaces)) {
-                                       for (var i = 0; i < info.inbound_interfaces.length; i++) {
-                                               var iif = info.inbound_interfaces[i];
+                                       for (let i = 0; i < info.inbound_interfaces.length; i++) {
+                                               const iif = info.inbound_interfaces[i];
 
-                                               for (var j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
-                                                       var chg = this.changes.network[j];
+                                               for (let j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
+                                                       const chg = this.changes.network[j];
 
                                                        if (chg[0] == 'set' && chg[1] == iif && (chg[2] == 'proto' || chg[2] == 'ipaddr' || chg[2] == 'netmask'))
                                                                return iif;
@@ -4543,13 +4546,13 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               rollback: function(checked) {
+               rollback(checked) {
                        if (checked) {
                                this.displayStatus('warning spinning',
                                        E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
                                                .format(L.env.apply_rollback)));
 
-                               var call = function(r, data, duration) {
+                               const call = (r, data, duration) => {
                                        if (r.status === 204) {
                                                UI.prototype.changes.displayStatus('warning', [
                                                        E('h4', _('Configuration changes have been rolled back!')),
@@ -4573,8 +4576,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                return;
                                        }
 
-                                       var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
-                                       window.setTimeout(function() {
+                                       const delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+                                       window.setTimeout(() => {
                                                request.request(L.url('admin/uci/confirm'), {
                                                        method: 'post',
                                                        timeout: L.env.apply_timeout * 1000,
@@ -4594,16 +4597,16 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                },
 
                /** @private */
-               confirm: function(checked, deadline, override_token) {
-                       var tt;
-                       var ts = Date.now();
+               confirm(checked, deadline, override_token) {
+                       let tt;
+                       let ts = Date.now();
 
                        this.displayStatus('notice');
 
                        if (override_token)
                                this.confirm_auth = { token: override_token };
 
-                       var call = function(r, data, duration) {
+                       const call = (r, data, duration) => {
                                if (Date.now() >= deadline) {
                                        window.clearTimeout(tt);
                                        UI.prototype.changes.rollback(checked);
@@ -4617,7 +4620,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                E('p', _('Configuration changes applied.')));
 
                                        window.clearTimeout(tt);
-                                       window.setTimeout(function() {
+                                       window.setTimeout(() => {
                                                //UI.prototype.changes.displayStatus(false);
                                                window.location = window.location.href.split('#')[0];
                                        }, L.env.apply_display * 1000);
@@ -4625,8 +4628,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                        return;
                                }
 
-                               var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
-                               window.setTimeout(function() {
+                               const delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+                               window.setTimeout(() => {
                                        request.request(L.url('admin/uci/confirm'), {
                                                method: 'post',
                                                timeout: L.env.apply_timeout * 1000,
@@ -4635,8 +4638,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                }, delay);
                        };
 
-                       var tick = function() {
-                               var now = Date.now();
+                       const tick = () => {
+                               const now = Date.now();
 
                                UI.prototype.changes.displayStatus('notice spinning',
                                        E('p', _('Applying configuration changes… %ds')
@@ -4675,15 +4678,15 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * will begin to roll back the changes in order to restore the previous
                 * settings.
                 */
-               apply: function(checked) {
+               apply(checked) {
                        this.displayStatus('notice spinning',
                                E('p', _('Starting configuration apply…')));
 
-                       (new Promise(function(resolveFn, rejectFn) {
+                       (new Promise((resolveFn, rejectFn) => {
                                if (!checked)
                                        return resolveFn(false);
 
-                               UI.prototype.changes.checkConnectivityAffected().then(function(affected) {
+                               UI.prototype.changes.checkConnectivityAffected().then(affected => {
                                        if (!affected)
                                                return resolveFn(true);
 
@@ -4707,13 +4710,13 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                ])
                                        ]);
                                });
-                       })).then(function(checked) {
+                       })).then(checked => {
                                request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
                                        method: 'post',
                                        query: { sid: L.env.sessionid, token: L.env.token }
-                               }).then(function(r) {
+                               }).then(r => {
                                        if (r.status === (checked ? 200 : 204)) {
-                                               var tok = null; try { tok = r.json(); } catch(e) {}
+                                               let tok = null; try { tok = r.json(); } catch(e) {}
                                                if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
                                                        UI.prototype.changes.confirm_auth = tok;
 
@@ -4723,16 +4726,16 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                                UI.prototype.changes.displayStatus('notice',
                                                        E('p', _('There are no changes to apply')));
 
-                                               window.setTimeout(function() {
+                                               window.setTimeout(() => {
                                                        UI.prototype.changes.displayStatus(false);
                                                }, L.env.apply_display * 1000);
                                        }
                                        else {
                                                UI.prototype.changes.displayStatus('warning',
                                                        E('p', _('Apply request failed with status <code>%h</code>')
-                                                               .format(r.responseText || r.statusText || r.status)));
+                                                               .format(r.responseText ?? r.statusText ?? r.status)));
 
-                                               window.setTimeout(function() {
+                                               window.setTimeout(() => {
                                                        UI.prototype.changes.displayStatus(false);
                                                }, L.env.apply_display * 1000);
                                        }
@@ -4752,14 +4755,14 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                 * @instance
                 * @memberof LuCI.ui.changes
                 */
-               revert: function() {
+               revert() {
                        this.displayStatus('notice spinning',
                                E('p', _('Reverting configuration…')));
 
                        request.request(L.url('admin/uci/revert'), {
                                method: 'post',
                                query: { sid: L.env.sessionid, token: L.env.token }
-                       }).then(function(r) {
+                       }).then(r => {
                                if (r.status === 200) {
                                        document.dispatchEvent(new CustomEvent('uci-reverted'));
 
@@ -4767,7 +4770,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                        UI.prototype.changes.displayStatus('notice',
                                                E('p', _('Changes have been reverted.')));
 
-                                       window.setTimeout(function() {
+                                       window.setTimeout(() => {
                                                //UI.prototype.changes.displayStatus(false);
                                                window.location = window.location.href.split('#')[0];
                                        }, L.env.apply_display * 1000);
@@ -4775,9 +4778,9 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                else {
                                        UI.prototype.changes.displayStatus('warning',
                                                E('p', _('Revert request failed with status <code>%h</code>')
-                                                       .format(r.statusText || r.status)));
+                                                       .format(r.statusText ?? r.status)));
 
-                                       window.setTimeout(function() {
+                                       window.setTimeout(() => {
                                                UI.prototype.changes.displayStatus(false);
                                        }, L.env.apply_display * 1000);
                                }
@@ -4820,19 +4823,18 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         *
         * @see LuCI.validation
         */
-       addValidator: function(field, type, optional, vfunc /*, ... */) {
+       addValidator(field, type, optional, vfunc, ...events) {
                if (type == null)
                        return;
 
-               var events = this.varargs(arguments, 3);
                if (events.length == 0)
                        events.push('blur', 'keyup');
 
                try {
-                       var cbiValidator = validation.create(field, type, optional, vfunc),
-                           validatorFn = cbiValidator.validate.bind(cbiValidator);
+                       const cbiValidator = validation.create(field, type, optional, vfunc);
+                       const validatorFn = cbiValidator.validate.bind(cbiValidator);
 
-                       for (var i = 0; i < events.length; i++)
+                       for (let i = 0; i < events.length; i++)
                                field.addEventListener(events[i], validatorFn);
 
                        validatorFn();
@@ -4871,17 +4873,15 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * a string which could not be found in `ctx` or if `ctx[fn]` is not a
         * valid function value.
         */
-       createHandlerFn: function(ctx, fn /*, ... */) {
+       createHandlerFn(ctx, fn, ...args) {
                if (typeof(fn) == 'string')
                        fn = ctx[fn];
 
                if (typeof(fn) != 'function')
                        return null;
 
-               var arg_offset = arguments.length - 2;
-
-               return Function.prototype.bind.apply(function() {
-                       var t = arguments[arg_offset].currentTarget;
+               return L.bind(function() {
+                       const t = arguments[args.length].currentTarget;
 
                        t.classList.add('spinning');
                        t.disabled = true;
@@ -4889,11 +4889,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                        if (t.blur)
                                t.blur();
 
-                       Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
+                       Promise.resolve(fn.apply(ctx, arguments)).finally(() => {
                                t.classList.remove('spinning');
                                t.disabled = false;
                        });
-               }, this.varargs(arguments, 2, ctx));
+               }, ctx, ...args);
        },
 
        /**
@@ -4914,15 +4914,15 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
         * @returns {Promise<LuCI.view>}
         * Returns a promise resolving to the loaded view instance.
         */
-       instantiateView: function(path) {
-               var className = 'view.%s'.format(path.replace(/\//g, '.'));
+       instantiateView(path) {
+               const className = 'view.%s'.format(path.replace(/\//g, '.'));
 
-               return L.require(className).then(function(view) {
+               return L.require(className).then(view => {
                        if (!(view instanceof View))
                                throw new TypeError('Loaded class %s is not a descendant of View'.format(className));
 
                        return view;
-               }).catch(function(err) {
+               }).catch(err => {
                        dom.content(document.querySelector('#view'), null);
                        L.error(err);
                });
index 791a84823d4c1ec49157bb22e6165bdc01842d81..b8dd5be8ef372ac28ab25867452539873c3a41aa 100644 (file)
@@ -5,10 +5,10 @@ function bytelen(x) {
        return new Blob([x]).size;
 }
 
-var Validator = baseclass.extend({
+const Validator = baseclass.extend({
        __name__: 'Validation',
 
-       __init__: function(field, type, optional, vfunc, validatorFactory) {
+       __init__(field, type, optional, vfunc, validatorFactory) {
                this.field = field;
                this.optional = optional;
                this.vfunc = vfunc;
@@ -16,7 +16,7 @@ var Validator = baseclass.extend({
                this.factory = validatorFactory;
        },
 
-       assert: function(condition, message) {
+       assert(condition, message) {
                if (!condition) {
                        this.field.classList.add('cbi-input-invalid');
                        this.error = message;
@@ -28,8 +28,8 @@ var Validator = baseclass.extend({
                return true;
        },
 
-       apply: function(name, value, args) {
-               var func;
+       apply(name, value, args) {
+               let func;
 
                if (typeof(name) === 'function')
                        func = name;
@@ -44,7 +44,7 @@ var Validator = baseclass.extend({
                return func.apply(this, args);
        },
 
-       validate: function() {
+       validate() {
                /* element is detached */
                if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
                        return true;
@@ -53,7 +53,7 @@ var Validator = baseclass.extend({
                this.value = (this.field.value != null) ? this.field.value : '';
                this.error = null;
 
-               var valid;
+               let valid;
 
                if (this.value.length === 0)
                        valid = this.assert(this.optional, _('non-empty value'));
@@ -61,7 +61,7 @@ var Validator = baseclass.extend({
                        valid = this.vstack[0].apply(this, this.vstack[1]);
 
                if (valid !== true) {
-                       var message = _('Expecting: %s').format(this.error);
+                       const message = _('Expecting: %s').format(this.error);
                        this.field.setAttribute('data-tooltip', message);
                        this.field.setAttribute('data-tooltip-style', 'error');
                        this.field.dispatchEvent(new CustomEvent('validation-failure', {
@@ -97,22 +97,22 @@ var Validator = baseclass.extend({
 
 });
 
-var ValidatorFactory = baseclass.extend({
+const ValidatorFactory = baseclass.extend({
        __name__: 'ValidatorFactory',
 
-       create: function(field, type, optional, vfunc) {
+       create(field, type, optional, vfunc) {
                return new Validator(field, type, optional, vfunc, this);
        },
 
-       compile: function(code) {
-               var pos = 0;
-               var esc = false;
-               var depth = 0;
-               var stack = [ ];
+       compile(code) {
+               let pos = 0;
+               let esc = false;
+               let depth = 0;
+               const stack = [ ];
 
                code += ',';
 
-               for (var i = 0; i < code.length; i++) {
+               for (let i = 0; i < code.length; i++) {
                        if (esc) {
                                esc = false;
                                continue;
@@ -128,7 +128,7 @@ var ValidatorFactory = baseclass.extend({
                        case 44:
                                if (depth <= 0) {
                                        if (pos < i) {
-                                               var label = code.substring(pos, i);
+                                               let label = code.substring(pos, i);
                                                        label = label.replace(/\\(.)/g, '$1');
                                                        label = label.replace(/^[ \t]+/g, '');
                                                        label = label.replace(/[ \t]+$/g, '');
@@ -170,15 +170,15 @@ var ValidatorFactory = baseclass.extend({
                return stack;
        },
 
-       parseInteger: function(x) {
+       parseInteger(x) {
                return (/^-?\d+$/.test(x) ? +x : NaN);
        },
 
-       parseDecimal: function(x) {
+       parseDecimal(x) {
                return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
        },
 
-       parseIPv4: function(x) {
+       parseIPv4(x) {
                if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
                        return null;
 
@@ -188,34 +188,35 @@ var ValidatorFactory = baseclass.extend({
                return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
        },
 
-       parseIPv6: function(x) {
+       parseIPv6(x) {
                if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
-                       var v6 = RegExp.$1, v4 = this.parseIPv4(RegExp.$2);
+                       const v6 = RegExp.$1;
+                       const v4 = this.parseIPv4(RegExp.$2);
 
                        if (!v4)
                                return null;
 
-                       x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
-                              + ':' + (v4[2] * 256 + v4[3]).toString(16);
+                       x = `${v6}:${(v4[0] * 256 + v4[1]).toString(16)}:${(v4[2] * 256 + v4[3]).toString(16)}`;
                }
 
                if (!x.match(/^[a-fA-F0-9:]+$/))
                        return null;
 
-               var prefix_suffix = x.split(/::/);
+               const prefix_suffix = x.split(/::/);
 
                if (prefix_suffix.length > 2)
                        return null;
 
-               var prefix = (prefix_suffix[0] || '0').split(/:/);
-               var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
+               const prefix = (prefix_suffix[0] || '0').split(/:/);
+               const suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
 
                if (suffix.length ? (prefix.length + suffix.length > 7)
-                                 : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
+                                     : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
                        return null;
 
-               var i, word;
-               var words = [];
+               let i;
+               let word;
+               const words = [];
 
                for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
                        if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
@@ -236,112 +237,112 @@ var ValidatorFactory = baseclass.extend({
        },
 
        types: {
-               integer: function() {
+               integer() {
                        return this.assert(!isNaN(this.factory.parseInteger(this.value)), _('valid integer value'));
                },
 
-               uinteger: function() {
+               uinteger() {
                        return this.assert(this.factory.parseInteger(this.value) >= 0, _('positive integer value'));
                },
 
-               float: function() {
+               float() {
                        return this.assert(!isNaN(this.factory.parseDecimal(this.value)), _('valid decimal value'));
                },
 
-               ufloat: function() {
+               ufloat() {
                        return this.assert(this.factory.parseDecimal(this.value) >= 0, _('positive decimal value'));
                },
 
-               ipaddr: function(nomask) {
+               ipaddr(nomask) {
                        return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]),
                                nomask ? _('valid IP address') : _('valid IP address or prefix'));
                },
 
-               ip4addr: function(nomask) {
-                       var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/,
-                           m = this.value.match(re);
+               ip4addr(nomask) {
+                       const re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/;
+                       const m = this.value.match(re);
 
                        return this.assert(m && this.factory.parseIPv4(m[1]) && (m[2] ? this.factory.parseIPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
                                nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
                },
 
-               ip6addr: function(nomask) {
-                       var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
-                           m = this.value.match(re);
+               ip6addr(nomask) {
+                       const re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/;
+                       const m = this.value.match(re);
 
                        return this.assert(m && this.factory.parseIPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
                                nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
                },
 
-               ip4prefix: function() {
+               ip4prefix() {
                        return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
                                _('valid IPv4 prefix value (0-32)'));
                },
 
-               ip6prefix: function() {
+               ip6prefix() {
                        return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
                                _('valid IPv6 prefix value (0-128)'));
                },
 
-               cidr: function(negative) {
+               cidr(negative) {
                        return this.assert(this.apply('cidr4', null, [negative]) || this.apply('cidr6', null, [negative]),
                                _('valid IPv4 or IPv6 CIDR'));
                },
 
-               cidr4: function(negative) {
-                       var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/);
+               cidr4(negative) {
+                       const m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/);
                        return this.assert(m && this.factory.parseIPv4(m[1]) && (negative || !m[2]) && this.apply('ip4prefix', m[3]),
                                _('valid IPv4 CIDR'));
                },
 
-               cidr6: function(negative) {
-                       var m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/);
+               cidr6(negative) {
+                       const m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/);
                        return this.assert(m && this.factory.parseIPv6(m[1]) && (negative || !m[2]) && this.apply('ip6prefix', m[3]),
                                _('valid IPv6 CIDR'));
                },
 
-               ipnet4: function() {
-                       var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
+               ipnet4() {
+                       const m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
                        return this.assert(m && this.factory.parseIPv4(m[1]) && this.factory.parseIPv4(m[2]), _('IPv4 network in address/netmask notation'));
                },
 
-               ipnet6: function() {
-                       var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
+               ipnet6() {
+                       const m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
                        return this.assert(m && this.factory.parseIPv6(m[1]) && this.factory.parseIPv6(m[2]), _('IPv6 network in address/netmask notation'));
                },
 
-               ip6hostid: function() {
+               ip6hostid() {
                        if (this.value == "eui64" || this.value == "random")
                                return true;
 
-                       var v6 = this.factory.parseIPv6(this.value);
+                       const v6 = this.factory.parseIPv6(this.value);
                        return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id'));
                },
 
-               ipmask: function(negative) {
+               ipmask(negative) {
                        return this.assert(this.apply('ipmask4', null, [negative]) || this.apply('ipmask6', null, [negative]),
                                _('valid network in address/netmask notation'));
                },
 
-               ipmask4: function(negative) {
+               ipmask4(negative) {
                        return this.assert(this.apply('cidr4', null, [negative]) || this.apply('ipnet4') || this.apply('ip4addr'),
                                _('valid IPv4 network'));
                },
 
-               ipmask6: function(negative) {
+               ipmask6(negative) {
                        return this.assert(this.apply('cidr6', null, [negative]) || this.apply('ipnet6') || this.apply('ip6addr'),
                                _('valid IPv6 network'));
                },
 
-               port: function() {
-                       var p = this.factory.parseInteger(this.value);
+               port() {
+                       const p = this.factory.parseInteger(this.value);
                        return this.assert(p >= 0 && p <= 65535, _('valid port value'));
                },
 
-               portrange: function() {
+               portrange() {
                        if (this.value.match(/^(\d+)-(\d+)$/)) {
-                               var p1 = +RegExp.$1;
-                               var p2 = +RegExp.$2;
+                               const p1 = +RegExp.$1;
+                               const p2 = +RegExp.$2;
                                return this.assert(p1 <= p2 && p2 <= 65535,
                                        _('valid port or port range (port1-port2)'));
                        }
@@ -349,18 +350,18 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
                },
 
-               macaddr: function(multicast) {
-                       var m = this.value.match(/^([a-fA-F0-9]{2}):([a-fA-F0-9]{2}:){4}[a-fA-F0-9]{2}$/);
+               macaddr(multicast) {
+                       const m = this.value.match(/^([a-fA-F0-9]{2}):([a-fA-F0-9]{2}:){4}[a-fA-F0-9]{2}$/);
                        return this.assert(m != null && !(+m[1] & 1) == !multicast,
                                multicast ? _('valid multicast MAC address') : _('valid MAC address'));
                },
 
-               host: function(ipv4only) {
+               host(ipv4only) {
                        return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr', null, ['nomask']),
                                _('valid hostname or IP address'));
                },
 
-               hostname: function(strict) {
+               hostname(strict) {
                        if (this.value.length <= 253)
                                return this.assert(
                                        (this.value.match(/^[a-zA-Z0-9_]+$/) != null ||
@@ -372,26 +373,26 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(false, _('valid hostname'));
                },
 
-               network: function() {
+               network() {
                        return this.assert(this.apply('uciname') || this.apply('hostname') || this.apply('ip4addr') || this.apply('ip6addr'),
                                _('valid UCI identifier, hostname or IP address range'));
                },
 
-               hostport: function(ipv4only) {
-                       var hp = this.value.split(/:/);
+               hostport(ipv4only) {
+                       const hp = this.value.split(/:/);
                        return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]),
                                _('valid host:port'));
                },
 
-               ip4addrport: function() {
-                       var hp = this.value.split(/:/);
+               ip4addrport() {
+                       const hp = this.value.split(/:/);
                        return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]),
                                _('valid IPv4 address:port'));
                },
 
-               ipaddrport: function(bracket) {
-                       var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/),
-                           m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
+               ipaddrport(bracket) {
+                       const m4 = this.value.match(/^([^\[\]:]+):(\d+)$/);
+                       const m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
 
                        if (m4)
                                return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]),
@@ -401,8 +402,8 @@ var ValidatorFactory = baseclass.extend({
                                _('valid address:port'));
                },
 
-               wpakey: function() {
-                       var v = this.value;
+               wpakey() {
+                       const v = this.value;
 
                        if (v.length == 64)
                                return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key'));
@@ -410,8 +411,8 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters'));
                },
 
-               wepkey: function() {
-                       var v = this.value;
+               wepkey() {
+                       let v = this.value;
 
                        if (v.substr(0, 2) === 's:')
                                v = v.substr(2);
@@ -422,12 +423,12 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters'));
                },
 
-               uciname: function() {
+               uciname() {
                        return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
                },
 
-               netdevname: function() {
-                       var v = this.value;
+               netdevname() {
+                       const v = this.value;
 
                        if (v == '.' || v == '..')
                                return this.assert(false, _('valid network device name, not "." or ".."'));
@@ -435,44 +436,44 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(v.match(/^[^:\/%\s]{1,15}$/), _('valid network device name between 1 and 15 characters not containing ":", "/", "%" or spaces'));
                },
 
-               range: function(min, max) {
-                       var val = this.factory.parseDecimal(this.value);
+               range(min, max) {
+                       const val = this.factory.parseDecimal(this.value);
                        return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
                },
 
-               min: function(min) {
+               min(min) {
                        return this.assert(this.factory.parseDecimal(this.value) >= +min, _('value greater or equal to %f').format(min));
                },
 
-               max: function(max) {
+               max(max) {
                        return this.assert(this.factory.parseDecimal(this.value) <= +max, _('value smaller or equal to %f').format(max));
                },
 
-               length: function(len) {
+               length(len) {
                        return this.assert(bytelen(this.value) == +len,
                                _('value with %d characters').format(len));
                },
 
-               rangelength: function(min, max) {
-                       var len = bytelen(this.value);
+               rangelength(min, max) {
+                       const len = bytelen(this.value);
                        return this.assert((len >= +min) && (len <= +max),
                                _('value between %d and %d characters').format(min, max));
                },
 
-               minlength: function(min) {
+               minlength(min) {
                        return this.assert(bytelen(this.value) >= +min,
                                _('value with at least %d characters').format(min));
                },
 
-               maxlength: function(max) {
+               maxlength(max) {
                        return this.assert(bytelen(this.value) <= +max,
                                _('value with at most %d characters').format(max));
                },
 
-               or: function() {
-                       var errors = [];
+               or() {
+                       const errors = [];
 
-                       for (var i = 0; i < arguments.length; i += 2) {
+                       for (let i = 0; i < arguments.length; i += 2) {
                                if (typeof arguments[i] != 'function') {
                                        if (arguments[i] == this.value)
                                                return this.assert(true);
@@ -487,13 +488,13 @@ var ValidatorFactory = baseclass.extend({
                                }
                        }
 
-                       var t = _('One of the following: %s');
+                       const t = _('One of the following: %s');
 
-                       return this.assert(false, t.format('\n - ' + errors.join('\n - ')));
+                       return this.assert(false, t.format(`\n - ${errors.join('\n - ')}`));
                },
 
-               and: function() {
-                       for (var i = 0; i < arguments.length; i += 2) {
+               and() {
+                       for (let i = 0; i < arguments.length; i += 2) {
                                if (typeof arguments[i] != 'function') {
                                        if (arguments[i] != this.value)
                                                return this.assert(false, '"%s"'.format(arguments[i]));
@@ -507,7 +508,7 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(true);
                },
 
-               neg: function() {
+               neg() {
                        this.value = this.value.replace(/^[ \t]*![ \t]*/, '');
 
                        if (arguments[0].apply(this, arguments[1]))
@@ -516,64 +517,58 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(false, _('Potential negation of: %s').format(this.error));
                },
 
-               list: function(subvalidator, subargs) {
+               list(subvalidator, subargs) {
                        this.field.setAttribute('data-is-list', 'true');
 
-                       var tokens = this.value.match(/[^ \t]+/g);
-                       for (var i = 0; i < tokens.length; i++)
+                       const tokens = this.value.match(/[^ \t]+/g);
+                       for (let i = 0; i < tokens.length; i++)
                                if (!this.apply(subvalidator, tokens[i], subargs))
                                        return this.assert(false, this.error);
 
                        return this.assert(true);
                },
 
-               phonedigit: function() {
+               phonedigit() {
                        return this.assert(this.value.match(/^[0-9\*#!\.]+$/),
                                _('valid phone digit (0-9, "*", "#", "!" or ".")'));
                },
 
-               timehhmmss: function() {
+               timehhmmss() {
                        return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/),
                                _('valid time (HH:MM:SS)'));
                },
 
-               dateyyyymmdd: function() {
+               dateyyyymmdd() {
                        if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
-                               var year  = +RegExp.$1,
-                                   month = +RegExp.$2,
-                                   day   = +RegExp.$3,
-                                   days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+                               const year  = +RegExp.$1;
+                               const month = +RegExp.$2;
+                               const day   = +RegExp.$3;
+                               const days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
-                               var is_leap_year = function(year) {
-                                       return ((!(year % 4) && (year % 100)) || !(year % 400));
-                               }
-
-                               var get_days_in_month = function(month, year) {
-                                       return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
-                               }
+                               const is_leap_year = year => (!(year % 4) && (year % 100)) || !(year % 400);
+                               const get_days_in_month = (month, year) => (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
 
                                /* Firewall rules in the past don't make sense */
                                return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year),
                                        _('valid date (YYYY-MM-DD)'));
-
                        }
 
                        return this.assert(false, _('valid date (YYYY-MM-DD)'));
                },
 
-               unique: function(subvalidator, subargs) {
-                       var ctx = this,
-                           option = findParent(ctx.field, '[data-widget][data-name]'),
-                           section = findParent(option, '.cbi-section'),
-                           query = '[data-widget="%s"][data-name="%s"]'.format(option.getAttribute('data-widget'), option.getAttribute('data-name')),
-                           unique = true;
+               unique(subvalidator, subargs) {
+                       const ctx = this;
+                       const option = findParent(ctx.field, '[data-widget][data-name]');
+                       const section = findParent(option, '.cbi-section');
+                       const query = '[data-widget="%s"][data-name="%s"]'.format(option.getAttribute('data-widget'), option.getAttribute('data-name'));
+                       let unique = true;
 
-                       section.querySelectorAll(query).forEach(function(sibling) {
+                       section.querySelectorAll(query).forEach(sibling => {
                                if (sibling === option)
                                        return;
 
-                               var input = sibling.querySelector('[data-type]'),
-                                   values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
+                               const input = sibling.querySelector('[data-type]');
+                               const values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
 
                                if (values !== null && values.indexOf(ctx.value) !== -1)
                                        unique = false;
@@ -588,24 +583,24 @@ var ValidatorFactory = baseclass.extend({
                        return this.assert(true);
                },
 
-               hexstring: function() {
+               hexstring() {
                        return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
                                _('hexadecimal encoded value'));
                },
 
-               string: function() {
+               string() {
                        return true;
                },
 
-               directory: function() {
+               directory() {
                        return true;
                },
 
-               file: function() {
+               file() {
                        return true;
                },
 
-               device: function() {
+               device() {
                        return true;
                }
        }