luci-base: harmonize JS class naming and requesting
authorJo-Philipp Wich <jo@mein.io>
Thu, 2 Apr 2020 19:40:50 +0000 (21:40 +0200)
committerJo-Philipp Wich <jo@mein.io>
Thu, 2 Apr 2020 19:51:20 +0000 (21:51 +0200)
 - Make builtin classes available via `require` to allow view code to
   request external and internal classes in a consistent manner without
   having to know which classes are builtin and which not

 - Make base classes request any used class explicitely instead of
   relying on implicitly set up L.{dom,view,Poll,Request,Class} aliases

 - Consistently convert class names to lower case in JSdoc to match
   the names used in `require` statements

 - Deprecate L.{dom,view,Poll,Request,Class} aliases

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/fs.js
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/htdocs/luci-static/resources/network.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 917584bb8231889a0e10a1a90c1db14706528e2f..69793ee55432bde24b52c84617dc565ef7a43b82 100644 (file)
@@ -1,10 +1,12 @@
 'use strict';
 'require ui';
 'require uci';
+'require dom';
+'require baseclass';
 
 var scope = this;
 
-var CBIJSONConfig = Class.extend({
+var CBIJSONConfig = baseclass.extend({
        __init__: function(data) {
                data = Object.assign({}, data);
 
@@ -171,7 +173,7 @@ var CBIJSONConfig = Class.extend({
        }
 });
 
-var CBINode = Class.extend({
+var CBINode = baseclass.extend({
        __init__: function(title, description) {
                this.title = title || '';
                this.description = description || '';
@@ -330,12 +332,12 @@ var CBIMap = CBINode.extend({
                        'cbi-dependency-check': L.bind(this.checkDepends, this)
                }));
 
-               L.dom.bindClassInstance(mapEl, this);
+               dom.bindClassInstance(mapEl, this);
 
                return this.renderChildren(null).then(L.bind(function(nodes) {
                        var initialRender = !mapEl.firstChild;
 
-                       L.dom.content(mapEl, null);
+                       dom.content(mapEl, null);
 
                        if (this.title != null && this.title != '')
                                mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
@@ -344,9 +346,9 @@ var CBIMap = CBINode.extend({
                                mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
 
                        if (this.tabbed)
-                               L.dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
+                               dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
                        else
-                               L.dom.append(mapEl, nodes);
+                               dom.append(mapEl, nodes);
 
                        if (!initialRender) {
                                mapEl.classList.remove('flash');
@@ -377,7 +379,7 @@ var CBIMap = CBINode.extend({
 
                elem = this.findElement('data-field', id);
                sid  = elem ? id.split(/\./)[2] : null;
-               inst = elem ? L.dom.findClassInstance(elem) : null;
+               inst = elem ? dom.findClassInstance(elem) : null;
 
                return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
        },
@@ -764,7 +766,7 @@ var CBIAbstractValue = CBINode.extend({
 
                var node = this.map.findElement('id', this.cbid(section_id));
                if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
-                       L.dom.callClassMethod(node, 'setValue', satisified_defval);
+                       dom.callClassMethod(node, 'setValue', satisified_defval);
 
                this.default = satisified_defval;
        },
@@ -790,7 +792,7 @@ var CBIAbstractValue = CBINode.extend({
 
        getUIElement: function(section_id) {
                var node = this.map.findElement('id', this.cbid(section_id)),
-                   inst = node ? L.dom.findClassInstance(node) : null;
+                   inst = node ? dom.findClassInstance(node) : null;
                return (inst instanceof ui.AbstractElement) ? inst : null;
        },
 
@@ -929,7 +931,7 @@ var CBITypedSection = CBIAbstractSection.extend({
                        createEl.appendChild(E('button', {
                                'class': 'cbi-button cbi-button-add',
                                'title': btn_title || _('Add'),
-                               'click': L.ui.createHandlerFn(this, 'handleAdd')
+                               'click': ui.createHandlerFn(this, 'handleAdd')
                        }, [ btn_title || _('Add') ]));
                }
                else {
@@ -938,14 +940,14 @@ var CBITypedSection = CBIAbstractSection.extend({
                                'class': 'cbi-section-create-name'
                        });
 
-                       L.dom.append(createEl, [
+                       dom.append(createEl, [
                                E('div', {}, nameEl),
                                E('input', {
                                        'class': 'cbi-button cbi-button-add',
                                        'type': 'submit',
                                        'value': btn_title || _('Add'),
                                        'title': btn_title || _('Add'),
-                                       'click': L.ui.createHandlerFn(this, function(ev) {
+                                       'click': ui.createHandlerFn(this, function(ev) {
                                                if (nameEl.classList.contains('cbi-input-invalid'))
                                                        return;
 
@@ -991,7 +993,7 @@ var CBITypedSection = CBIAbstractSection.extend({
                                                        'class': 'cbi-button',
                                                        'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
                                                        'data-section-id': cfgsections[i],
-                                                       'click': L.ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
+                                                       'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
                                                }, [ _('Delete') ])));
                        }
 
@@ -1011,7 +1013,7 @@ var CBITypedSection = CBIAbstractSection.extend({
 
                sectionEl.appendChild(this.renderSectionAdd());
 
-               L.dom.bindClassInstance(sectionEl, this);
+               dom.bindClassInstance(sectionEl, this);
 
                return sectionEl;
        },
@@ -1099,7 +1101,7 @@ var CBITableSection = CBITypedSection.extend({
 
                sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
 
-               L.dom.bindClassInstance(sectionEl, this);
+               dom.bindClassInstance(sectionEl, this);
 
                return sectionEl;
        },
@@ -1146,7 +1148,7 @@ var CBITableSection = CBITypedSection.extend({
                                                'title': this.titledesc || _('Go to relevant configuration page')
                                        }, opt.title));
                                else
-                                       L.dom.content(trEl.lastElementChild, opt.title);
+                                       dom.content(trEl.lastElementChild, opt.title);
                        }
 
                        if (this.sortable || this.extedit || this.addremove || has_more || has_action)
@@ -1198,7 +1200,7 @@ var CBITableSection = CBITypedSection.extend({
                }, E('div'));
 
                if (this.sortable) {
-                       L.dom.append(tdEl.lastElementChild, [
+                       dom.append(tdEl.lastElementChild, [
                                E('div', {
                                        'title': _('Drag to reorder'),
                                        'class': 'btn cbi-button drag-handle center',
@@ -1217,7 +1219,7 @@ var CBITableSection = CBITypedSection.extend({
                                        location.href = this.extedit.format(sid);
                                }, this, section_id);
 
-                       L.dom.append(tdEl.lastElementChild,
+                       dom.append(tdEl.lastElementChild,
                                E('button', {
                                        'title': _('Edit'),
                                        'class': 'cbi-button cbi-button-edit',
@@ -1227,11 +1229,11 @@ var CBITableSection = CBITypedSection.extend({
                }
 
                if (more_label) {
-                       L.dom.append(tdEl.lastElementChild,
+                       dom.append(tdEl.lastElementChild,
                                E('button', {
                                        'title': more_label,
                                        'class': 'cbi-button cbi-button-edit',
-                                       'click': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
+                                       'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
                                }, [ more_label ])
                        );
                }
@@ -1239,11 +1241,11 @@ var CBITableSection = CBITypedSection.extend({
                if (this.addremove) {
                        var btn_title = this.titleFn('removebtntitle', section_id);
 
-                       L.dom.append(tdEl.lastElementChild,
+                       dom.append(tdEl.lastElementChild,
                                E('button', {
                                        'title': btn_title || _('Delete'),
                                        'class': 'cbi-button cbi-button-remove',
-                                       'click': L.ui.createHandlerFn(this, 'handleRemove', section_id)
+                                       'click': ui.createHandlerFn(this, 'handleRemove', section_id)
                                }, [ btn_title || _('Delete') ])
                        );
                }
@@ -1262,7 +1264,7 @@ var CBITableSection = CBITypedSection.extend({
                        return false;
                }
 
-               scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
+               scope.dragState.node = dom.parent(scope.dragState.node, '.tr');
                ev.dataTransfer.setData('text', 'drag');
                ev.target.style.opacity = 0.4;
        },
@@ -1336,14 +1338,14 @@ var CBITableSection = CBITypedSection.extend({
        },
 
        handleModalCancel: function(modalMap, ev) {
-               return Promise.resolve(L.ui.hideModal());
+               return Promise.resolve(ui.hideModal());
        },
 
        handleModalSave: function(modalMap, ev) {
                return modalMap.save()
                        .then(L.bind(this.map.load, this.map))
                        .then(L.bind(this.map.reset, this.map))
-                       .then(L.ui.hideModal)
+                       .then(ui.hideModal)
                        .catch(function() {});
        },
 
@@ -1397,16 +1399,16 @@ var CBITableSection = CBITypedSection.extend({
                }
 
                return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
-                       L.ui.showModal(title, [
+                       ui.showModal(title, [
                                nodes,
                                E('div', { 'class': 'right' }, [
                                        E('button', {
                                                'class': 'btn',
-                                               'click': L.ui.createHandlerFn(this, 'handleModalCancel', m)
+                                               'click': ui.createHandlerFn(this, 'handleModalCancel', m)
                                        }, [ _('Dismiss') ]), ' ',
                                        E('button', {
                                                'class': 'cbi-button cbi-button-positive important',
-                                               'click': L.ui.createHandlerFn(this, 'handleModalSave', m)
+                                               'click': ui.createHandlerFn(this, 'handleModalSave', m)
                                        }, [ _('Save') ])
                                ])
                        ], 'cbi-modal');
@@ -1555,7 +1557,7 @@ var CBINamedSection = CBIAbstractSection.extend({
                                        E('div', { 'class': 'cbi-section-remove right' },
                                                E('button', {
                                                        'class': 'cbi-button',
-                                                       'click': L.ui.createHandlerFn(this, 'handleRemove')
+                                                       'click': ui.createHandlerFn(this, 'handleRemove')
                                                }, [ _('Delete') ])));
                        }
 
@@ -1570,11 +1572,11 @@ var CBINamedSection = CBIAbstractSection.extend({
                        sectionEl.appendChild(
                                E('button', {
                                        'class': 'cbi-button cbi-button-add',
-                                       'click': L.ui.createHandlerFn(this, 'handleAdd')
+                                       'click': ui.createHandlerFn(this, 'handleAdd')
                                }, [ _('Add') ]));
                }
 
-               L.dom.bindClassInstance(sectionEl, this);
+               dom.bindClassInstance(sectionEl, this);
 
                return sectionEl;
        },
@@ -1598,7 +1600,7 @@ var CBIValue = CBIAbstractValue.extend({
                this.keylist.push(String(key));
 
                this.vallist = this.vallist || [];
-               this.vallist.push(L.dom.elem(val) ? val : String(val != null ? val : key));
+               this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
        },
 
        render: function(option_index, section_id, in_table) {
@@ -1669,7 +1671,7 @@ var CBIValue = CBIAbstractValue.extend({
                        (optionEl.lastChild || optionEl).appendChild(nodes);
 
                if (!in_table && typeof(this.description) === 'string' && this.description !== '')
-                       L.dom.append(optionEl.lastChild || optionEl,
+                       dom.append(optionEl.lastChild || optionEl,
                                E('div', { 'class': 'cbi-value-description' }, this.description));
 
                if (depend_list && depend_list.length)
@@ -1678,7 +1680,7 @@ var CBIValue = CBIAbstractValue.extend({
                optionEl.addEventListener('widget-change',
                        L.bind(this.map.checkDepends, this.map));
 
-               L.dom.bindClassInstance(optionEl, this);
+               dom.bindClassInstance(optionEl, this);
 
                return optionEl;
        },
@@ -1877,7 +1879,7 @@ var CBIDummyValue = CBIValue.extend({
                if (this.href)
                        outputEl.appendChild(E('a', { 'href': this.href }));
 
-               L.dom.append(outputEl.lastChild || outputEl,
+               dom.append(outputEl.lastChild || outputEl,
                        this.rawhtml ? value : [ value ]);
 
                return E([
@@ -1900,10 +1902,10 @@ var CBIButtonValue = CBIValue.extend({
                    btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
 
                if (value !== false)
-                       L.dom.content(outputEl, [
+                       dom.content(outputEl, [
                                E('button', {
                                        'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
-                                       'click': L.ui.createHandlerFn(this, function(section_id, ev) {
+                                       'click': ui.createHandlerFn(this, function(section_id, ev) {
                                                if (this.onclick)
                                                        return this.onclick(ev, section_id);
 
@@ -1913,7 +1915,7 @@ var CBIButtonValue = CBIValue.extend({
                                }, [ btn_title ])
                        ]);
                else
-                       L.dom.content(outputEl, ' - ');
+                       dom.content(outputEl, ' - ');
 
                return E([
                        outputEl,
@@ -1995,7 +1997,7 @@ var CBISectionValue = CBIValue.extend({
        formvalue: function() { return null }
 });
 
-return L.Class.extend({
+return baseclass.extend({
        Map: CBIMap,
        JSONMap: CBIJSONMap,
        AbstractSection: CBIAbstractSection,
index 8d2760dd5e87b01837a5ef13f246d3242a648071..99defb76c526010c401f922286c88a526583d068 100644 (file)
@@ -1,5 +1,7 @@
 'use strict';
 'require rpc';
+'require request';
+'require baseclass';
 
 /**
  * @typedef {Object} FileStatEntry
@@ -152,7 +154,7 @@ function handleCgiIoReply(res) {
  * To import the class in views, use `'require fs'`, to import it in
  * external JavaScript, use `L.require("fs").then(...)`.
  */
-var FileSystem = L.Class.extend(/** @lends LuCI.fs.prototype */ {
+var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
        /**
         * Obtains a listing of the specified directory.
         *
@@ -357,7 +359,7 @@ var FileSystem = L.Class.extend(/** @lends LuCI.fs.prototype */ {
                var postdata = 'sessionid=%s&path=%s'
                        .format(encodeURIComponent(L.env.sessionid), encodeURIComponent(path));
 
-               return L.Request.post(L.env.cgi_base + '/cgi-download', postdata, {
+               return request.post(L.env.cgi_base + '/cgi-download', postdata, {
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        responseType: (type == 'blob') ? 'blob' : 'text'
                }).then(handleCgiIoReply.bind({ type: type }));
@@ -417,7 +419,7 @@ var FileSystem = L.Class.extend(/** @lends LuCI.fs.prototype */ {
                var postdata = 'sessionid=%s&command=%s'
                        .format(encodeURIComponent(L.env.sessionid), cmdstr);
 
-               return L.Request.post(L.env.cgi_base + '/cgi-exec', postdata, {
+               return request.post(L.env.cgi_base + '/cgi-exec', postdata, {
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        responseType: (type == 'blob') ? 'blob' : 'text'
                }).then(handleCgiIoReply.bind({ type: type }));
index fd4c584886a25a48d30499fa537a608efbb88729..5984ad184afc758dc627f397f367d771bac445b4 100644 (file)
        };
 
        /**
-        * @class Class
+        * @class baseclass
         * @hideconstructor
         * @memberof LuCI
         * @classdesc
         *
-        * `LuCI.Class` is the abstract base class all LuCI classes inherit from.
+        * `LuCI.baseclass` is the abstract base class all LuCI classes inherit from.
         *
         * It provides simple means to create subclasses of given classes and
         * implements prototypal inheritance.
                 * Extends this base class with the properties described in
                 * `properties` and returns a new subclassed Class instance
                 *
-                * @memberof LuCI.Class
+                * @memberof LuCI.baseclass
                 *
                 * @param {Object<string, *>} properties
                 * An object describing the properties to add to the new
                 * subclass.
                 *
-                * @returns {LuCI.Class}
-                * Returns a new LuCI.Class sublassed from this class, extended
+                * @returns {LuCI.baseclass}
+                * Returns a new LuCI.baseclass sublassed from this class, extended
                 * by the given properties and with its prototype set to this base
                 * class to enable inheritance. The resulting value represents a
                 * class constructor and can be instantiated with `new`.
                 * and returns the resulting subclassed Class instance.
                 *
                 * This function serves as a convenience shortcut for
-                * {@link LuCI.Class.extend Class.extend()} and subsequent
+                * {@link LuCI.baseclass.extend Class.extend()} and subsequent
                 * `new`.
                 *
-                * @memberof LuCI.Class
+                * @memberof LuCI.baseclass
                 *
                 * @param {Object<string, *>} properties
                 * An object describing the properties to add to the new
                 * Specifies arguments to be passed to the subclass constructor
                 * as-is in order to instantiate the new subclass.
                 *
-                * @returns {LuCI.Class}
-                * Returns a new LuCI.Class instance extended by the given
+                * @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.
                 */
                 * Calls the class constructor using `new` with the given argument
                 * array being passed as variadic parameters to the constructor.
                 *
-                * @memberof LuCI.Class
+                * @memberof LuCI.baseclass
                 *
                 * @param {Array<*>} params
                 * An array of arbitrary values which will be passed as arguments
                 * Specifies arguments to be passed to the subclass constructor
                 * as-is in order to instantiate the new subclass.
                 *
-                * @returns {LuCI.Class}
-                * Returns a new LuCI.Class instance extended by the given
+                * @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.
                 */
                /**
                 * Checks whether the given class value is a subclass of this class.
                 *
-                * @memberof LuCI.Class
+                * @memberof LuCI.baseclass
                 *
-                * @param {LuCI.Class} classValue
+                * @param {LuCI.baseclass} classValue
                 * The class object to test.
                 *
                 * @returns {boolean}
                         * `offset` and prepend any further given optional parameters to
                         * the beginning of the resulting array copy.
                         *
-                        * @memberof LuCI.Class
+                        * @memberof LuCI.baseclass
                         * @instance
                         *
                         * @param {Array<*>} args
                         *    Calls the `key()` method with parameters `arg1` and `arg2`
                         *    when found within one of the parent classes.
                         *
-                        * @memberof LuCI.Class
+                        * @memberof LuCI.baseclass
                         * @instance
                         *
                         * @param {string} key
 
 
        /**
-        * @class
+        * @class headers
         * @memberof LuCI
         * @hideconstructor
         * @classdesc
         * 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 */ {
-               __name__: 'LuCI.XHR.Headers',
+       var 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) {
                 * Note: Header-Names are case-insensitive.
                 *
                 * @instance
-                * @memberof LuCI.Headers
+                * @memberof LuCI.headers
                 * @param {string} name
                 * The header name to check
                 *
                 * Note: Header-Names are case-insensitive.
                 *
                 * @instance
-                * @memberof LuCI.Headers
+                * @memberof LuCI.headers
                 * @param {string} name
                 * The header name to read
                 *
        });
 
        /**
-        * @class
+        * @class response
         * @memberof LuCI
         * @hideconstructor
         * @classdesc
         * The `Response` class is an internal utility class representing HTTP responses.
         */
        var Response = Class.extend({
-               __name__: 'LuCI.XHR.Response',
+               __name__: 'LuCI.response',
                __init__: function(xhr, url, duration, headers, content) {
                        /**
                         * Describes whether the response is successful (status codes `200..299`) or not
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name ok
                         * @type {boolean}
                         */
                        /**
                         * The numeric HTTP status code of the response
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name status
                         * @type {number}
                         */
                        /**
                         * The HTTP status description message of the response
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name statusText
                         * @type {string}
                         */
                        /**
                         * The HTTP headers of the response
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name headers
-                        * @type {LuCI.Headers}
+                        * @type {LuCI.headers}
                         */
                        this.headers = (headers != null) ? headers : new Headers(xhr);
 
                        /**
                         * The total duration of the HTTP request in milliseconds
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name duration
                         * @type {number}
                         */
                        /**
                         * The final URL of the request, i.e. after following redirects.
                         * @instance
-                        * @memberof LuCI.Response
+                        * @memberof LuCI.response
                         * @name url
                         * @type {string}
                         */
                 * of the cloned instance.
                 *
                 * @instance
-                * @memberof LuCI.Response
+                * @memberof LuCI.response
                 * @param {*} [content]
                 * Override the content of the cloned response. Object values will be
                 * treated as JSON response data, all other types will be converted
                 * using `String()` and treated as response text.
                 *
-                * @returns {LuCI.Response}
+                * @returns {LuCI.response}
                 * The cloned `Response` instance.
                 */
                clone: function(content) {
                 * Access the response content as JSON data.
                 *
                 * @instance
-                * @memberof LuCI.Response
+                * @memberof LuCI.response
                 * @throws {SyntaxError}
                 * Throws `SyntaxError` if the content isn't valid JSON.
                 *
                 * Access the response content as string.
                 *
                 * @instance
-                * @memberof LuCI.Response
+                * @memberof LuCI.response
                 * @returns {string}
                 * The response content.
                 */
                 * Access the response content as blob.
                 *
                 * @instance
-                * @memberof LuCI.Response
+                * @memberof LuCI.response
                 * @returns {Blob}
                 * The response content as blob.
                 */
        }
 
        /**
-        * @class
+        * @class request
         * @memberof LuCI
         * @hideconstructor
         * @classdesc
         * The `Request` class allows initiating HTTP requests and provides utilities
         * for dealing with responses.
         */
-       var Request = Class.singleton(/** @lends LuCI.Request.prototype */ {
-               __name__: 'LuCI.Request',
+       var Request = Class.singleton(/** @lends LuCI.request.prototype */ {
+               __name__: 'LuCI.request',
 
                interceptors: [],
 
                 * Turn the given relative URL into an absolute URL if necessary.
                 *
                 * @instance
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 * @param {string} url
                 * The URL to convert.
                 *
 
                /**
                 * @typedef {Object} RequestOptions
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 *
                 * @property {string} [method=GET]
                 * The HTTP method to use, e.g. `GET` or `POST`.
                 * Initiate an HTTP request to the given target.
                 *
                 * @instance
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 * @param {string} target
                 * The URL to request.
                 *
-                * @param {LuCI.Request.RequestOptions} [options]
+                * @param {LuCI.request.RequestOptions} [options]
                 * Additional options to configure the request.
                 *
-                * @returns {Promise<LuCI.Response>}
+                * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
                request: function(target, options) {
                 * Initiate an HTTP GET request to the given target.
                 *
                 * @instance
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 * @param {string} target
                 * The URL to request.
                 *
-                * @param {LuCI.Request.RequestOptions} [options]
+                * @param {LuCI.request.RequestOptions} [options]
                 * Additional options to configure the request.
                 *
-                * @returns {Promise<LuCI.Response>}
+                * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
                get: function(url, options) {
                 * Initiate an HTTP POST request to the given target.
                 *
                 * @instance
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 * @param {string} target
                 * The URL to request.
                 *
                 * @param {*} [data]
-                * The request data to send, see {@link LuCI.Request.RequestOptions} for details.
+                * The request data to send, see {@link LuCI.request.RequestOptions} for details.
                 *
-                * @param {LuCI.Request.RequestOptions} [options]
+                * @param {LuCI.request.RequestOptions} [options]
                 * Additional options to configure the request.
                 *
-                * @returns {Promise<LuCI.Response>}
+                * @returns {Promise<LuCI.response>}
                 * The resulting HTTP response.
                 */
                post: function(url, data, options) {
                /**
                 * Interceptor functions are invoked whenever an HTTP reply is received, in the order
                 * these functions have been registered.
-                * @callback LuCI.Request.interceptorFn
-                * @param {LuCI.Response} res
+                * @callback LuCI.request.interceptorFn
+                * @param {LuCI.response} res
                 * The HTTP response object
                 */
 
                 * implementing request retries before returning a failure.
                 *
                 * @instance
-                * @memberof LuCI.Request
-                * @param {LuCI.Request.interceptorFn} interceptorFn
+                * @memberof LuCI.request
+                * @param {LuCI.request.interceptorFn} interceptorFn
                 * The interceptor function to register.
                 *
-                * @returns {LuCI.Request.interceptorFn}
+                * @returns {LuCI.request.interceptorFn}
                 * The registered function.
                 */
                addInterceptor: function(interceptorFn) {
                 * function.
                 *
                 * @instance
-                * @memberof LuCI.Request
-                * @param {LuCI.Request.interceptorFn} interceptorFn
+                * @memberof LuCI.request
+                * @param {LuCI.request.interceptorFn} interceptorFn
                 * The interceptor function to remove.
                 *
                 * @returns {boolean}
 
                /**
                 * @class
-                * @memberof LuCI.Request
+                * @memberof LuCI.request
                 * @hideconstructor
                 * @classdesc
                 *
                 * The `Request.poll` class provides some convience wrappers around
-                * {@link LuCI.Poll} mainly to simplify registering repeating HTTP
+                * {@link LuCI.poll} mainly to simplify registering repeating HTTP
                 * request calls as polling functions.
                 */
                poll: {
                         * polled request is received or when the polled request timed
                         * out.
                         *
-                        * @callback LuCI.Request.poll~callbackFn
-                        * @param {LuCI.Response} res
+                        * @callback LuCI.request.poll~callbackFn
+                        * @param {LuCI.response} res
                         * The HTTP response object.
                         *
                         * @param {*} data
                         * to invoke whenever a response for the request is received.
                         *
                         * @instance
-                        * @memberof LuCI.Request.poll
+                        * @memberof LuCI.request.poll
                         * @param {number} interval
                         * The poll interval in seconds.
                         *
                         * @param {string} url
                         * The URL to request on each poll.
                         *
-                        * @param {LuCI.Request.RequestOptions} [options]
+                        * @param {LuCI.request.RequestOptions} [options]
                         * Additional options to configure the request.
                         *
-                        * @param {LuCI.Request.poll~callbackFn} [callback]
-                        * {@link LuCI.Request.poll~callbackFn Callback} function to
+                        * @param {LuCI.request.poll~callbackFn} [callback]
+                        * {@link LuCI.request.poll~callbackFn Callback} function to
                         * invoke for each HTTP reply.
                         *
                         * @throws {TypeError}
                        /**
                         * Remove a polling request that has been previously added using `add()`.
                         * This function is essentially a wrapper around
-                        * {@link LuCI.Poll.remove LuCI.Poll.remove()}.
+                        * {@link LuCI.poll.remove LuCI.poll.remove()}.
                         *
                         * @instance
-                        * @memberof LuCI.Request.poll
+                        * @memberof LuCI.request.poll
                         * @param {function} entry
-                        * The poll function returned by {@link LuCI.Request.poll#add add()}.
+                        * The poll function returned by {@link LuCI.request.poll#add add()}.
                         *
                         * @returns {boolean}
                         * Returns `true` if any function has been removed, else `false`.
                        remove: function(entry) { return Poll.remove(entry) },
 
                        /**
-                         * Alias for {@link LuCI.Poll.start LuCI.Poll.start()}.
+                         * Alias for {@link LuCI.poll.start LuCI.poll.start()}.
                          *
                          * @instance
-                         * @memberof LuCI.Request.poll
+                         * @memberof LuCI.request.poll
                          */
                        start: function() { return Poll.start() },
 
                        /**
-                         * Alias for {@link LuCI.Poll.stop LuCI.Poll.stop()}.
+                         * Alias for {@link LuCI.poll.stop LuCI.poll.stop()}.
                          *
                          * @instance
-                         * @memberof LuCI.Request.poll
+                         * @memberof LuCI.request.poll
                          */
                        stop: function() { return Poll.stop() },
 
                        /**
-                         * Alias for {@link LuCI.Poll.active LuCI.Poll.active()}.
+                         * Alias for {@link LuCI.poll.active LuCI.poll.active()}.
                          *
                          * @instance
-                         * @memberof LuCI.Request.poll
+                         * @memberof LuCI.request.poll
                          */
                        active: function() { return Poll.active() }
                }
        });
 
        /**
-        * @class
+        * @class poll
         * @memberof LuCI
         * @hideconstructor
         * @classdesc
         * as well as starting, stopping and querying the state of the polling
         * loop.
         */
-       var Poll = Class.singleton(/** @lends LuCI.Poll.prototype */ {
-               __name__: 'LuCI.Poll',
+       var Poll = Class.singleton(/** @lends LuCI.poll.prototype */ {
+               __name__: 'LuCI.poll',
 
                queue: [],
 
                 * already started at this point, it will be implicitely started.
                 *
                 * @instance
-                * @memberof LuCI.Poll
+                * @memberof LuCI.poll
                 * @param {function} fn
                 * The function to invoke on each poll interval.
                 *
                                interval = window.L ? window.L.env.pollinterval : null;
 
                        if (isNaN(interval) || typeof(fn) != 'function')
-                               throw new TypeError('Invalid argument to LuCI.Poll.add()');
+                               throw new TypeError('Invalid argument to LuCI.poll.add()');
 
                        for (var i = 0; i < this.queue.length; i++)
                                if (this.queue[i].fn === fn)
                 * are registered, the polling loop is implicitely stopped.
                 *
                 * @instance
-                * @memberof LuCI.Poll
+                * @memberof LuCI.poll
                 * @param {function} fn
                 * The function to remove.
                 *
                 */
                remove: function(fn) {
                        if (typeof(fn) != 'function')
-                               throw new TypeError('Invalid argument to LuCI.Poll.remove()');
+                               throw new TypeError('Invalid argument to LuCI.poll.remove()');
 
                        var len = this.queue.length;
 
                 * to the `document` object upon successful start.
                 *
                 * @instance
-                * @memberof LuCI.Poll
+                * @memberof LuCI.poll
                 * @returns {boolean}
                 * Returns `true` if polling has been started (or if no functions
                 * where registered) or `false` when the polling loop already runs.
                 * to the `document` object upon successful stop.
                 *
                 * @instance
-                * @memberof LuCI.Poll
+                * @memberof LuCI.poll
                 * @returns {boolean}
                 * Returns `true` if polling has been stopped or `false` if it din't
                 * run to begin with.
                 * Test whether the polling loop is running.
                 *
                 * @instance
-                * @memberof LuCI.Poll
+                * @memberof LuCI.poll
                 * @returns {boolean} - Returns `true` if polling is active, else `false`.
                 */
                active: function() {
                }
        });
 
+       /**
+        * @class dom
+        * @memberof LuCI
+        * @hideconstructor
+        * @classdesc
+        *
+        * The `dom` class provides convenience method for creating and
+        * manipulating DOM elements.
+        *
+        * 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 */ {
+               __name__: 'LuCI.dom',
 
-       var dummyElem = null,
-           domParser = null,
-           originalCBIInit = null,
-           rpcBaseURL = null,
-           sysFeatures = null,
-           classes = {};
-
-       var LuCI = Class.extend(/** @lends LuCI.prototype */ {
-               __name__: 'LuCI',
-               __init__: function(env) {
-
-                       document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
-                               if (env.base_url == null || env.base_url == '') {
-                                       var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
-                                       if (m) {
-                                               env.base_url = m[1];
-                                               env.resource_version = m[2];
-                                       }
-                               }
-                       });
-
-                       if (env.base_url == null)
-                               this.error('InternalError', 'Cannot find url of luci.js');
-
-                       env.cgi_base = env.scriptname.replace(/\/[^\/]+$/, '');
-
-                       Object.assign(this.env, env);
-
-                       document.addEventListener('poll-start', function(ev) {
-                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
-                                       e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
-                               });
-                       });
+               /**
+                * Tests whether the given argument is a valid DOM `Node`.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {*} e
+                * The value to test.
+                *
+                * @returns {boolean}
+                * Returns `true` if the value is a DOM `Node`, else `false`.
+                */
+               elem: function(e) {
+                       return (e != null && typeof(e) == 'object' && 'nodeType' in e);
+               },
 
-                       document.addEventListener('poll-stop', function(ev) {
-                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
-                                       e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
-                               });
-                       });
+               /**
+                * Parses a given string as HTML and returns the first child node.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {string} s
+                * A string containing an HTML fragment to parse. Note that only
+                * the first result of the resulting structure is returned, so an
+                * input value of `<div>foo</div> <div>bar</div>` will only return
+                * the first `div` element node.
+                *
+                * @returns {Node}
+                * 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;
 
-                       var domReady = new Promise(function(resolveFn, rejectFn) {
-                               document.addEventListener('DOMContentLoaded', resolveFn);
-                       });
+                       try {
+                               domParser = domParser || new DOMParser();
+                               elem = domParser.parseFromString(s, 'text/html').body.firstChild;
+                       }
+                       catch(e) {}
 
-                       Promise.all([
-                               domReady,
-                               this.require('ui'),
-                               this.require('rpc'),
-                               this.require('form'),
-                               this.probeRPCBaseURL()
-                       ]).then(this.setupDOM.bind(this)).catch(this.error);
+                       if (!elem) {
+                               try {
+                                       dummyElem = dummyElem || document.createElement('div');
+                                       dummyElem.innerHTML = s;
+                                       elem = dummyElem.firstChild;
+                               }
+                               catch (e) {}
+                       }
 
-                       originalCBIInit = window.cbi_init;
-                       window.cbi_init = function() {};
+                       return elem || null;
                },
 
                /**
-                * Captures the current stack trace and throws an error of the
-                * specified type as a new exception. Also logs the exception as
-                * error to the debug console if it is available.
+                * Tests whether a given `Node` matches the given query selector.
+                *
+                * This function is a convenience wrapper around the standard
+                * `Node.matches("selector")` function with the added benefit that
+                * the `node` argument may be a non-`Node` value, in which case
+                * this function simply returns `false`.
                 *
                 * @instance
-                * @memberof LuCI
+                * @memberof LuCI.dom
+                * @param {*} node
+                * The `Node` argument to test the selector against.
                 *
-                * @param {Error|string} [type=Error]
-                * Either a string specifying the type of the error to throw or an
-                * existing `Error` instance to copy.
+                * @param {string} [selector]
+                * The query selector expression to test against the given node.
                 *
-                * @param {string} [fmt=Unspecified error]
-                * A format string which is used to form the error message, together
-                * with all subsequent optional arguments.
+                * @returns {boolean}
+                * Returns `true` if the given node matches the specified selector
+                * 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;
+                       return m ? m.call(node, selector) : false;
+               },
+
+               /**
+                * Returns the closest parent node that matches the given query
+                * selector expression.
                 *
-                * @param {...*} [args]
-                * Zero or more variable arguments to the supplied format string.
+                * This function is a convenience wrapper around the standard
+                * `Node.closest("selector")` function with the added benefit that
+                * the `node` argument may be a non-`Node` value, in which case
+                * this function simply returns `null`.
                 *
-                * @throws {Error}
-                * Throws the created error object with the captured stack trace
-                * appended to the message and the type set to the given type
-                * argument or copied from the given error instance.
+                * @instance
+                * @memberof LuCI.dom
+                * @param {*} node
+                * The `Node` argument to find the closest parent for.
+                *
+                * @param {string} [selector]
+                * The query selector expression to test against each parent.
+                *
+                * @returns {Node|null}
+                * Returns the closest parent node matching the selector or
+                * `null` when the node argument is no valid DOM `Node` or the
+                * selector didn't match any parent.
                 */
-               raise: function(type, fmt /*, ...*/) {
-                       var e = null,
-                           msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
-                           stack = null;
+               parent: function(node, selector) {
+                       if (this.elem(node) && node.closest)
+                               return node.closest(selector);
 
-                       if (type instanceof Error) {
-                               e = type;
+                       while (this.elem(node))
+                               if (this.matches(node, selector))
+                                       return node;
+                               else
+                                       node = node.parentNode;
 
-                               if (msg)
-                                       e.message = msg + ': ' + e.message;
-                       }
-                       else {
-                               try { throw new Error('stacktrace') }
-                               catch (e2) { stack = (e2.stack || '').split(/\n/) }
+                       return null;
+               },
 
-                               e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
-                               e.name = type || 'Error';
-                       }
+               /**
+                * Appends the given children data to the given node.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {*} node
+                * The `Node` argument to append the children to.
+                *
+                * @param {*} [children]
+                * The childrens to append to the given node.
+                *
+                * When `children` is an array, then each item of the array
+                * will be either appended as child element or text node,
+                * depending on whether the item is a DOM `Node` instance or
+                * some other non-`null` value. Non-`Node`, non-`null` values
+                * will be converted to strings first before being passed as
+                * argument to `createTextNode()`.
+                *
+                * When `children` is a function, it will be invoked with
+                * the passed `node` argument as sole parameter and the `append`
+                * function will be invoked again, with the given `node` argument
+                * as first and the return value of the `children` function as
+                * second parameter.
+                *
+                * When `children` is is a DOM `Node` instance, it will be
+                * appended to the given `node`.
+                *
+                * When `children` is any other non-`null` value, it will be
+                * converted to a string and appened to the `innerHTML` property
+                * of the given `node`.
+                *
+                * @returns {Node|null}
+                * Returns the last children `Node` appended to the node or `null`
+                * 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) {
+                       if (!this.elem(node))
+                               return null;
 
-                       stack = (stack || []).map(function(frame) {
-                               frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
-                               return frame ? '  ' + frame : '';
-                       });
+                       if (Array.isArray(children)) {
+                               for (var 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]));
 
-                       if (!/^  at /.test(stack[0]))
-                               stack.shift();
+                               return node.lastChild;
+                       }
+                       else if (typeof(children) === 'function') {
+                               return this.append(node, children(node));
+                       }
+                       else if (this.elem(children)) {
+                               return node.appendChild(children);
+                       }
+                       else if (children !== null && children !== undefined) {
+                               node.innerHTML = '' + children;
+                               return node.lastChild;
+                       }
 
-                       if (/\braise /.test(stack[0]))
-                               stack.shift();
+                       return null;
+               },
 
-                       if (/\berror /.test(stack[0]))
-                               stack.shift();
+               /**
+                * Replaces the content of the given node with the given children.
+                *
+                * This function first removes any children of the given DOM
+                * `Node` and then adds the given given children following the
+                * rules outlined below.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {*} node
+                * The `Node` argument to replace the children of.
+                *
+                * @param {*} [children]
+                * The childrens to replace into the given node.
+                *
+                * When `children` is an array, then each item of the array
+                * will be either appended as child element or text node,
+                * depending on whether the item is a DOM `Node` instance or
+                * some other non-`null` value. Non-`Node`, non-`null` values
+                * will be converted to strings first before being passed as
+                * argument to `createTextNode()`.
+                *
+                * When `children` is a function, it will be invoked with
+                * the passed `node` argument as sole parameter and the `append`
+                * function will be invoked again, with the given `node` argument
+                * as first and the return value of the `children` function as
+                * second parameter.
+                *
+                * When `children` is is a DOM `Node` instance, it will be
+                * appended to the given `node`.
+                *
+                * When `children` is any other non-`null` value, it will be
+                * converted to a string and appened to the `innerHTML` property
+                * of the given `node`.
+                *
+                * @returns {Node|null}
+                * Returns the last children `Node` appended to the node or `null`
+                * 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) {
+                       if (!this.elem(node))
+                               return null;
 
-                       if (stack.length)
-                               e.message += '\n' + stack.join('\n');
+                       var dataNodes = node.querySelectorAll('[data-idref]');
 
-                       if (window.console && console.debug)
-                               console.debug(e);
+                       for (var i = 0; i < dataNodes.length; i++)
+                               delete this.registry[dataNodes[i].getAttribute('data-idref')];
 
-                       throw e;
+                       while (node.firstChild)
+                               node.removeChild(node.firstChild);
+
+                       return this.append(node, children);
                },
 
                /**
-                * A wrapper around {@link LuCI#raise raise()} which also renders
-                * the error either as modal overlay when `ui.js` is already loaed
-                * or directly into the view body.
+                * Sets attributes or registers event listeners on element nodes.
                 *
                 * @instance
-                * @memberof LuCI
+                * @memberof LuCI.dom
+                * @param {*} node
+                * The `Node` argument to set the attributes or add the event
+                * listeners for. When the given `node` value is not a valid
+                * DOM `Node`, the function returns and does nothing.
+                *
+                * @param {string|Object<string, *>} key
+                * Specifies either the attribute or event handler name to use,
+                * or an object containing multiple key, value pairs which are
+                * each added to the node as either attribute or event handler,
+                * depending on the respective value.
                 *
-                * @param {Error|string} [type=Error]
-                * Either a string specifying the type of the error to throw or an
-                * existing `Error` instance to copy.
+                * @param {*} [val]
+                * Specifies the attribute value or event handler function to add.
+                * If the `key` parameter is an `Object`, this parameter will be
+                * ignored.
                 *
-                * @param {string} [fmt=Unspecified error]
-                * A format string which is used to form the error message, together
-                * with all subsequent optional arguments.
+                * When `val` is of type function, it will be registered as event
+                * handler on the given `node` with the `key` parameter being the
+                * event name.
                 *
-                * @param {...*} [args]
-                * Zero or more variable arguments to the supplied format string.
+                * When `val` is of type object, it will be serialized as JSON and
+                * added as attribute to the given `node`, using the given `key`
+                * as attribute name.
                 *
-                * @throws {Error}
-                * Throws the created error object with the captured stack trace
-                * appended to the message and the type set to the given type
-                * argument or copied from the given error instance.
+                * When `val` is of any other type, it will be added as attribute
+                * to the given `node` as-is, with the underlying `setAttribute()`
+                * call implicitely turning it into a string.
                 */
-               error: function(type, fmt /*, ...*/) {
-                       try {
-                               L.raise.apply(L, Array.prototype.slice.call(arguments));
-                       }
-                       catch (e) {
-                               if (!e.reported) {
-                                       if (L.ui)
-                                               L.ui.addNotification(e.name || _('Runtime error'),
-                                                       E('pre', {}, e.message), 'danger');
-                                       else
-                                               L.dom.content(document.querySelector('#maincontent'),
-                                                       E('pre', { 'class': 'alert-message error' }, e.message));
+               attr: function(node, key, val) {
+                       if (!this.elem(node))
+                               return null;
 
-                                       e.reported = true;
-                               }
+                       var attr = null;
 
-                               throw e;
+                       if (typeof(key) === 'object' && key !== null)
+                               attr = key;
+                       else if (typeof(key) === 'string')
+                               attr = {}, attr[key] = val;
+
+                       for (key in attr) {
+                               if (!attr.hasOwnProperty(key) || attr[key] == null)
+                                       continue;
+
+                               switch (typeof(attr[key])) {
+                               case 'function':
+                                       node.addEventListener(key, attr[key]);
+                                       break;
+
+                               case 'object':
+                                       node.setAttribute(key, JSON.stringify(attr[key]));
+                                       break;
+
+                               default:
+                                       node.setAttribute(key, attr[key]);
+                               }
                        }
                },
 
                /**
-                * Return a bound function using the given `self` as `this` context
-                * and any further arguments as parameters to the bound function.
+                * Creates a new DOM `Node` from the given `html`, `attr` and
+                * `data` parameters.
+                *
+                * This function has multiple signatures, it can be either invoked
+                * in the form `create(html[, attr[, data]])` or in the form
+                * `create(html[, data])`. The used variant is determined from the
+                * type of the second argument.
                 *
                 * @instance
-                * @memberof LuCI
+                * @memberof LuCI.dom
+                * @param {*} html
+                * Describes the node to create.
                 *
-                * @param {function} fn
-                * The function to bind.
+                * When the value of `html` is of type array, a `DocumentFragment`
+                * node is created and each item of the array is first converted
+                * to a DOM `Node` by passing it through `create()` and then added
+                * as child to the fragment.
                 *
-                * @param {*} self
-                * The value to bind as `this` context to the specified function.
+                * When the value of `html` is a DOM `Node` instance, no new
+                * element will be created but the node will be used as-is.
                 *
-                * @param {...*} [args]
-                * Zero or more variable arguments which are bound to the function
-                * as parameters.
+                * When the value of `html` is a string starting with `<`, it will
+                * be passed to `dom.parse()` and the resulting value is used.
                 *
-                * @returns {function}
-                * Returns the bound function.
+                * When the value of `html` is any other string, it will be passed
+                * to `document.createElement()` for creating a new DOM `Node` of
+                * the given name.
+                *
+                * @param {Object<string, *>} [attr]
+                * Specifies an Object of key, value pairs to set as attributes
+                * or event handlers on the created node. Refer to
+                * {@link LuCI.dom#attr dom.attr()} for details.
+                *
+                * @param {*} [data]
+                * Specifies children to append to the newly created element.
+                * Refer to {@link LuCI.dom#append dom.append()} for details.
+                *
+                * @throws {InvalidCharacterError}
+                * Throws an `InvalidCharacterError` when the given `html`
+                * argument contained malformed markup (such as not escaped
+                * `&` characters in XHTML mode) or when the given node name
+                * in `html` contains characters which are not legal in DOM
+                * element names, such as spaces.
+                *
+                * @returns {Node}
+                * Returns the newly created `Node`.
                 */
-               bind: function(fn, self /*, ... */) {
-                       return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
+               create: function() {
+                       var html = arguments[0],
+                           attr = arguments[1],
+                           data = arguments[2],
+                           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++)
+                                       elem.appendChild(this.create(html[i]));
+                       }
+                       else if (this.elem(html)) {
+                               elem = html;
+                       }
+                       else if (html.charCodeAt(0) === 60) {
+                               elem = this.parse(html);
+                       }
+                       else {
+                               elem = document.createElement(html);
+                       }
+
+                       if (!elem)
+                               return null;
+
+                       this.attr(elem, attr);
+                       this.append(elem, data);
+
+                       return elem;
                },
 
+               registry: {},
+
                /**
-                * Load an additional LuCI JavaScript class and its dependencies,
-                * instantiate it and return the resulting class instance. Each
-                * class is only loaded once. Subsequent attempts to load the same
-                * class will return the already instantiated class.
+                * Attaches or detaches arbitrary data to and from a DOM `Node`.
+                *
+                * This function is useful to attach non-string values or runtime
+                * data that is not serializable to DOM nodes. To decouple data
+                * from the DOM, values are not added directly to nodes, but
+                * inserted into a registry instead which is then referenced by a
+                * string key stored as `data-idref` attribute in the node.
+                *
+                * This function has multiple signatures and is sensitive to the
+                * number of arguments passed to it.
+                *
+                *  - `dom.data(node)` -
+                *     Fetches all data associated with the given node.
+                *  - `dom.data(node, key)` -
+                *     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.
+                *  - `dom.data(node, null)` -
+                *     Clears any data associated with the node.
+                *  - `dom.data(node, key, null)` -
+                *     Clears the given key associated with the node.
                 *
                 * @instance
-                * @memberof LuCI
-                *
-                * @param {string} name
-                * The name of the class to load in dotted notation. Dots will
-                * be replaced by spaces and joined with the runtime-determined
-                * base URL of LuCI.js to form an absolute URL to load the class
-                * file from.
-                *
-                * @throws {DependencyError}
-                * Throws a `DependencyError` when the class to load includes
-                * circular dependencies.
-                *
-                * @throws {NetworkError}
-                * Throws `NetworkError` when the underlying {@link LuCI.Request}
-                * call failed.
+                * @memberof LuCI.dom
+                * @param {Node} node
+                * The DOM `Node` instance to set or retrieve the data for.
                 *
-                * @throws {SyntaxError}
-                * Throws `SyntaxError` when the loaded class file code cannot
-                * be interpreted by `eval`.
+                * @param {string|null} [key]
+                * This is either a string specifying the key to retrieve, or
+                * `null` to unset the entire node data.
                 *
-                * @throws {TypeError}
-                * Throws `TypeError` when the class file could be loaded and
-                * interpreted, but when invoking its code did not yield a valid
-                * class instance.
+                * @param {*|null} [val]
+                * This is either a non-`null` value to set for a given key or
+                * `null` to remove the given `key` from the specified node.
                 *
-                * @returns {Promise<LuCI#Class>}
-                * Returns the instantiated class.
+                * @returns {*}
+                * Returns the get or set value, or `null` when no value could
+                * be found.
                 */
-               require: function(name, from) {
-                       var L = this, url = null, from = from || [];
+               data: function(node, key, val) {
+                       if (!node || !node.getAttribute)
+                               return null;
 
-                       /* Class already loaded */
-                       if (classes[name] != null) {
-                               /* Circular dependency */
-                               if (from.indexOf(name) != -1)
-                                       L.raise('DependencyError',
-                                               'Circular dependency: class "%s" depends on "%s"',
-                                               name, from.join('" which depends on "'));
+                       var id = node.getAttribute('data-idref');
 
-                               return Promise.resolve(classes[name]);
+                       /* clear all data */
+                       if (arguments.length > 1 && key == null) {
+                               if (id != null) {
+                                       node.removeAttribute('data-idref');
+                                       val = this.registry[id]
+                                       delete this.registry[id];
+                                       return val;
+                               }
+
+                               return null;
                        }
 
-                       url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
-                       from = [ name ].concat(from);
-
-                       var compileClass = function(res) {
-                               if (!res.ok)
-                                       L.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 = '';
-
-                               /* find require statements in source */
-                               for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
-                                       var chr = source.charCodeAt(i);
+                       /* clear a key */
+                       else if (arguments.length > 2 && key != null && val == null) {
+                               if (id != null) {
+                                       val = this.registry[id][key];
+                                       delete this.registry[id][key];
+                                       return val;
+                               }
 
-                                       if (esc) {
-                                               esc = false;
-                                       }
-                                       else if (chr == 92) {
-                                               esc = true;
-                                       }
-                                       else if (chr == quote) {
-                                               var s = source.substring(off, i),
-                                                   m = requirematch.exec(s);
+                               return null;
+                       }
 
-                                               if (m) {
-                                                       var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
-                                                       depends.push(L.require(dep, from));
-                                                       args += ', ' + as;
-                                               }
-                                               else if (!strictmatch.exec(s)) {
-                                                       break;
-                                               }
+                       /* set a key */
+                       else if (arguments.length > 2 && key != null && val != null) {
+                               if (id == null) {
+                                       do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
+                                       while (this.registry.hasOwnProperty(id));
 
-                                               off = -1;
-                                               quote = -1;
-                                       }
-                                       else if (quote == -1 && (chr == 34 || chr == 39)) {
-                                               off = i + 1;
-                                               quote = chr;
-                                       }
+                                       node.setAttribute('data-idref', id);
+                                       this.registry[id] = {};
                                }
 
-                               /* load dependencies and instantiate class */
-                               return Promise.all(depends).then(function(instances) {
-                                       var _factory, _class;
+                               return (this.registry[id][key] = val);
+                       }
 
-                                       try {
-                                               _factory = eval(
-                                                       '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
-                                                               .format(args, source, res.url));
-                                       }
-                                       catch (error) {
-                                               L.raise('SyntaxError', '%s\n  in %s:%s',
-                                                       error.message, res.url, error.lineNumber || '?');
-                                       }
+                       /* get all data */
+                       else if (arguments.length == 1) {
+                               if (id != null)
+                                       return this.registry[id];
 
-                                       _factory.displayName = toCamelCase(name + 'ClassFactory');
-                                       _class = _factory.apply(_factory, [window, document, L].concat(instances));
+                               return null;
+                       }
 
-                                       if (!Class.isSubclass(_class))
-                                           L.error('TypeError', '"%s" factory yields invalid constructor', name);
+                       /* get a key */
+                       else if (arguments.length == 2) {
+                               if (id != null)
+                                       return this.registry[id][key];
+                       }
 
-                                       if (_class.displayName == 'AnonymousClass')
-                                               _class.displayName = toCamelCase(name + 'Class');
+                       return null;
+               },
 
-                                       var ptr = Object.getPrototypeOf(L),
-                                           parts = name.split(/\./),
-                                           instance = new _class();
+               /**
+                * Binds the given class instance ot the specified DOM `Node`.
+                *
+                * This function uses the `dom.data()` facility to attach the
+                * passed instance of a Class to a node. This is needed for
+                * complex widget elements or similar where the corresponding
+                * class instance responsible for the element must be retrieved
+                * from DOM nodes obtained by `querySelector()` or similar means.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {Node} node
+                * The DOM `Node` instance to bind the class to.
+                *
+                * @param {Class} inst
+                * The Class instance to bind to the node.
+                *
+                * @throws {TypeError}
+                * Throws a `TypeError` when the given instance argument isn't
+                * a valid Class instance.
+                *
+                * @returns {Class}
+                * Returns the bound class instance.
+                */
+               bindClassInstance: function(node, inst) {
+                       if (!(inst instanceof Class))
+                               L.error('TypeError', 'Argument must be a class instance');
 
-                                       for (var i = 0; ptr && i < parts.length - 1; i++)
-                                               ptr = ptr[parts[i]];
+                       return this.data(node, '_class', inst);
+               },
 
-                                       if (ptr)
-                                               ptr[parts[i]] = instance;
+               /**
+                * Finds a bound class instance on the given node itself or the
+                * first bound instance on its closest parent node.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {Node} node
+                * The DOM `Node` instance to start from.
+                *
+                * @returns {Class|null}
+                * 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;
 
-                                       classes[name] = instance;
+                       do {
+                               inst = this.data(node, '_class');
+                               node = node.parentNode;
+                       }
+                       while (!(inst instanceof Class) && node != null);
 
-                                       return instance;
-                               });
-                       };
+                       return inst;
+               },
 
-                       /* Request class file */
-                       classes[name] = Request.get(url, { cache: true }).then(compileClass);
+               /**
+                * Finds a bound class instance on the given node itself or the
+                * first bound instance on its closest parent node and invokes
+                * the specified method name on the found class instance.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {Node} node
+                * The DOM `Node` instance to start from.
+                *
+                * @param {string} method
+                * The name of the method to invoke on the found class instance.
+                *
+                * @param {...*} params
+                * Additional arguments to pass to the invoked method as-is.
+                *
+                * @returns {*|null}
+                * Returns the return value of the invoked method if a class
+                * instance and method has been found. Returns `null` if either
+                * 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);
 
-                       return classes[name];
+                       if (inst == null || typeof(inst[method]) != 'function')
+                               return null;
+
+                       return inst[method].apply(inst, inst.varargs(arguments, 2));
                },
 
-               /* DOM setup */
-               probeRPCBaseURL: function() {
-                       if (rpcBaseURL == null) {
-                               try {
-                                       rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
-                               }
-                               catch (e) { }
-                       }
+               /**
+                * The ignore callback function is invoked by `isEmpty()` for each
+                * child node to decide whether to ignore a child node or not.
+                *
+                * When this function returns `false`, the node passed to it is
+                * ignored, else not.
+                *
+                * @callback LuCI.dom~ignoreCallbackFn
+                * @param {Node} node
+                * The child node to test.
+                *
+                * @returns {boolean}
+                * Boolean indicating whether to ignore the node or not.
+                */
 
-                       if (rpcBaseURL == null) {
-                               var rpcFallbackURL = this.url('admin/ubus');
+               /**
+                * Tests whether a given DOM `Node` instance is empty or appears
+                * empty.
+                *
+                * Any element child nodes which have the CSS class `hidden` set
+                * or for which the optionally passed `ignoreFn` callback function
+                * returns `false` are ignored.
+                *
+                * @instance
+                * @memberof LuCI.dom
+                * @param {Node} node
+                * The DOM `Node` instance to test.
+                *
+                * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
+                * Specifies an optional function which is invoked for each child
+                * node to decide whether the child node should be ignored or not.
+                *
+                * @returns {boolean}
+                * Returns `true` if the node does not have any children or if
+                * 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)))
+                                       return false;
 
-                               rpcBaseURL = Request.get(this.env.ubuspath).then(function(res) {
-                                       return (rpcBaseURL = (res.status == 400) ? L.env.ubuspath : rpcFallbackURL);
-                               }, function() {
-                                       return (rpcBaseURL = rpcFallbackURL);
-                               }).then(function(url) {
-                                       try {
-                                               window.sessionStorage.setItem('rpcBaseURL', url);
-                                       }
-                                       catch (e) { }
+                       return true;
+               }
+       });
 
-                                       return url;
-                               });
-                       }
+       /**
+        * @class view
+        * @memberof LuCI
+        * @hideconstructor
+        * @classdesc
+        *
+        * 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 */ {
+               __name__: 'LuCI.view',
 
-                       return Promise.resolve(rpcBaseURL);
-               },
+               __init__: function() {
+                       var vp = document.getElementById('view');
 
-               probeSystemFeatures: function() {
-                       var sessionid = classes.rpc.getSessionID();
+                       DOM.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
 
-                       if (sysFeatures == null) {
-                               try {
-                                       var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
+                       return Promise.resolve(this.load())
+                               .then(L.bind(this.render, this))
+                               .then(L.bind(function(nodes) {
+                                       var vp = document.getElementById('view');
 
-                                       if (this.isObject(data) && this.isObject(data[sessionid]))
-                                               sysFeatures = data[sessionid];
-                               }
-                               catch (e) {}
-                       }
+                                       DOM.content(vp, nodes);
+                                       DOM.append(vp, this.addFooter());
+                               }, this)).catch(L.error);
+               },
 
-                       if (!this.isObject(sysFeatures)) {
-                               sysFeatures = classes.rpc.declare({
-                                       object: 'luci',
-                                       method: 'getFeatures',
-                                       expect: { '': {} }
-                               })().then(function(features) {
-                                       try {
-                                               var data = {};
-                                                   data[sessionid] = features;
+               /**
+                * The load function is invoked before the view is rendered.
+                *
+                * The invocation of this function is wrapped by
+                * `Promise.resolve()` so it may return Promises if needed.
+                *
+                * The return value of the function (or the resolved values
+                * of the promise returned by it) will be passed as first
+                * argument to `render()`.
+                *
+                * This function is supposed to be overwritten by subclasses,
+                * the default implementation does nothing.
+                *
+                * @instance
+                * @abstract
+                * @memberof LuCI.view
+                *
+                * @returns {*|Promise<*>}
+                * May return any value or a Promise resolving to any value.
+                */
+               load: function() {},
 
-                                               window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
-                                       }
-                                       catch (e) {}
+               /**
+                * The render function is invoked after the
+                * {@link LuCI.view#load load()} function and responsible
+                * for setting up the view contents. It must return a DOM
+                * `Node` or `DocumentFragment` holding the contents to
+                * insert into the view area.
+                *
+                * The invocation of this function is wrapped by
+                * `Promise.resolve()` so it may return Promises if needed.
+                *
+                * The return value of the function (or the resolved values
+                * of the promise returned by it) will be inserted into the
+                * main content area using
+                * {@link LuCI.dom#append dom.append()}.
+                *
+                * This function is supposed to be overwritten by subclasses,
+                * the default implementation does nothing.
+                *
+                * @instance
+                * @abstract
+                * @memberof LuCI.view
+                * @param {*|null} load_results
+                * This function will receive the return value of the
+                * {@link LuCI.view#load view.load()} function as first
+                * argument.
+                *
+                * @returns {Node|Promise<Node>}
+                * Should return a DOM `Node` value or a `Promise` resolving
+                * to a `Node` value.
+                */
+               render: function() {},
 
-                                       sysFeatures = features;
+               /**
+                * The handleSave function is invoked when the user clicks
+                * the `Save` button in the page action footer.
+                *
+                * The default implementation should be sufficient for most
+                * views using {@link form#Map form.Map()} based forms - it
+                * will iterate all forms present in the view and invoke
+                * the {@link form#Map#save Map.save()} method on each form.
+                *
+                * Views not using `Map` instances or requiring other special
+                * logic should overwrite `handleSave()` with a custom
+                * implementation.
+                *
+                * To disable the `Save` page footer button, views extending
+                * this base class should overwrite the `handleSave` function
+                * with `null`.
+                *
+                * The invocation of this function is wrapped by
+                * `Promise.resolve()` so it may return Promises if needed.
+                *
+                * @instance
+                * @memberof LuCI.view
+                * @param {Event} ev
+                * The DOM event that triggered the function.
+                *
+                * @returns {*|Promise<*>}
+                * Any return values of this function are discarded, but
+                * passed through `Promise.resolve()` to ensure that any
+                * returned promise runs to completion before the button
+                * is reenabled.
+                */
+               handleSave: function(ev) {
+                       var tasks = [];
 
-                                       return features;
+                       document.getElementById('maincontent')
+                               .querySelectorAll('.cbi-map').forEach(function(map) {
+                                       tasks.push(DOM.callClassMethod(map, 'save'));
                                });
-                       }
 
-                       return Promise.resolve(sysFeatures);
+                       return Promise.all(tasks);
                },
 
                /**
-                * Test whether a particular system feature is available, such as
-                * hostapd SAE support or an installed firewall. The features are
-                * queried once at the beginning of the LuCI session and cached in
-                * `SessionStorage` throughout the lifetime of the associated tab or
-                * browser window.
+                * The handleSaveApply function is invoked when the user clicks
+                * the `Save & Apply` button in the page action footer.
+                *
+                * The default implementation should be sufficient for most
+                * views using {@link form#Map form.Map()} based forms - it
+                * will first invoke
+                * {@link LuCI.view.handleSave view.handleSave()} and then
+                * call {@link ui#changes#apply ui.changes.apply()} to start the
+                * modal config apply and page reload flow.
+                *
+                * Views not using `Map` instances or requiring other special
+                * logic should overwrite `handleSaveApply()` with a custom
+                * implementation.
+                *
+                * To disable the `Save & Apply` page footer button, views
+                * extending this base class should overwrite the
+                * `handleSaveApply` function with `null`.
+                *
+                * The invocation of this function is wrapped by
+                * `Promise.resolve()` so it may return Promises if needed.
                 *
                 * @instance
-                * @memberof LuCI
+                * @memberof LuCI.view
+                * @param {Event} ev
+                * The DOM event that triggered the function.
+                *
+                * @returns {*|Promise<*>}
+                * Any return values of this function are discarded, but
+                * passed through `Promise.resolve()` to ensure that any
+                * returned promise runs to completion before the button
+                * is reenabled.
+                */
+               handleSaveApply: function(ev, mode) {
+                       return this.handleSave(ev).then(function() {
+                               L.ui.changes.apply(mode == '0');
+                       });
+               },
+
+               /**
+                * The handleReset function is invoked when the user clicks
+                * the `Reset` button in the page action footer.
                 *
-                * @param {string} feature
-                * The feature to test. For detailed list of known feature flags,
-                * see `/modules/luci-base/root/usr/libexec/rpcd/luci`.
+                * The default implementation should be sufficient for most
+                * views using {@link form#Map form.Map()} based forms - it
+                * will iterate all forms present in the view and invoke
+                * the {@link form#Map#save Map.reset()} method on each form.
                 *
-                * @param {string} [subfeature]
-                * Some feature classes like `hostapd` provide sub-feature flags,
-                * such as `sae` or `11w` support. The `subfeature` argument can
-                * be used to query these.
+                * Views not using `Map` instances or requiring other special
+                * logic should overwrite `handleReset()` with a custom
+                * implementation.
                 *
-                * @return {boolean|null}
-                * Return `true` if the queried feature (and sub-feature) is available
-                * or `false` if the requested feature isn't present or known.
-                * Return `null` when a sub-feature was queried for a feature which
-                * has no sub-features.
+                * To disable the `Reset` page footer button, views extending
+                * this base class should overwrite the `handleReset` function
+                * with `null`.
+                *
+                * The invocation of this function is wrapped by
+                * `Promise.resolve()` so it may return Promises if needed.
+                *
+                * @instance
+                * @memberof LuCI.view
+                * @param {Event} ev
+                * The DOM event that triggered the function.
+                *
+                * @returns {*|Promise<*>}
+                * Any return values of this function are discarded, but
+                * passed through `Promise.resolve()` to ensure that any
+                * returned promise runs to completion before the button
+                * is reenabled.
                 */
-               hasSystemFeature: function() {
-                       var ft = sysFeatures[arguments[0]];
+               handleReset: function(ev) {
+                       var tasks = [];
 
-                       if (arguments.length == 2)
-                               return this.isObject(ft) ? ft[arguments[1]] : null;
+                       document.getElementById('maincontent')
+                               .querySelectorAll('.cbi-map').forEach(function(map) {
+                                       tasks.push(DOM.callClassMethod(map, 'reset'));
+                               });
 
-                       return (ft != null && ft != false);
+                       return Promise.all(tasks);
                },
 
-               /* private */
-               notifySessionExpiry: function() {
-                       Poll.stop();
+               /**
+                * Renders a standard page action footer if any of the
+                * `handleSave()`, `handleSaveApply()` or `handleReset()`
+                * functions are defined.
+                *
+                * The default implementation should be sufficient for most
+                * views - it will render a standard page footer with action
+                * buttons labeled `Save`, `Save & Apply` and `Reset`
+                * triggering the `handleSave()`, `handleSaveApply()` and
+                * `handleReset()` functions respectively.
+                *
+                * When any of these `handle*()` functions is overwritten
+                * with `null` by a view extending this class, the
+                * corresponding button will not be rendered.
+                *
+                * @instance
+                * @memberof LuCI.view
+                * @returns {DocumentFragment}
+                * Returns a `DocumentFragment` containing the footer bar
+                * with buttons for each corresponding `handle*()` action
+                * or an empty `DocumentFragment` if all three `handle*()`
+                * methods are overwritten with `null`.
+                */
+               addFooter: function() {
+                       var footer = E([]);
+
+                       var saveApplyBtn = this.handleSaveApply ? new L.ui.ComboButton('0', {
+                               0: [ _('Save & Apply') ],
+                               1: [ _('Apply unchecked') ]
+                       }, {
+                               classes: {
+                                       0: 'btn cbi-button cbi-button-apply important',
+                                       1: 'btn cbi-button cbi-button-negative important'
+                               },
+                               click: L.ui.createHandlerFn(this, 'handleSaveApply')
+                       }).render() : E([]);
+
+                       if (this.handleSaveApply || this.handleSave || this.handleReset) {
+                               footer.appendChild(E('div', { 'class': 'cbi-page-actions control-group' }, [
+                                       saveApplyBtn, ' ',
+                                       this.handleSave ? E('button', {
+                                               'class': 'cbi-button cbi-button-save',
+                                               'click': L.ui.createHandlerFn(this, 'handleSave')
+                                       }, [ _('Save') ]) : '', ' ',
+                                       this.handleReset ? E('button', {
+                                               'class': 'cbi-button cbi-button-reset',
+                                               'click': L.ui.createHandlerFn(this, 'handleReset')
+                                       }, [ _('Reset') ]) : ''
+                               ]));
+                       }
 
-                       L.ui.showModal(_('Session expired'), [
-                               E('div', { class: 'alert-message warning' },
-                                       _('A new login is required since the authentication session expired.')),
-                               E('div', { class: 'right' },
-                                       E('div', {
-                                               class: 'btn primary',
-                                               click: function() {
-                                                       var loc = window.location;
-                                                       window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
-                                               }
-                                       }, _('To login…')))
-                       ]);
+                       return footer;
+               }
+       });
 
-                       L.raise('SessionError', 'Login session is expired');
-               },
 
-               /* private */
-               setupDOM: function(res) {
-                       var domEv = res[0],
-                           uiClass = res[1],
-                           rpcClass = res[2],
-                           formClass = res[3],
-                           rpcBaseURL = res[4];
+       var dummyElem = null,
+           domParser = null,
+           originalCBIInit = null,
+           rpcBaseURL = null,
+           sysFeatures = null;
+
+       /* "preload" builtin classes to make the available via require */
+       var classes = {
+               baseclass: Class,
+               dom: DOM,
+               poll: Poll,
+               request: Request,
+               view: View
+       };
 
-                       rpcClass.setBaseURL(rpcBaseURL);
+       var LuCI = Class.extend(/** @lends LuCI.prototype */ {
+               __name__: 'LuCI',
+               __init__: function(env) {
 
-                       rpcClass.addInterceptor(function(msg, req) {
-                               if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
-                                       return;
+                       document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
+                               if (env.base_url == null || env.base_url == '') {
+                                       var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
+                                       if (m) {
+                                               env.base_url = m[1];
+                                               env.resource_version = m[2];
+                                       }
+                               }
+                       });
 
-                               if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
-                                       return;
+                       if (env.base_url == null)
+                               this.error('InternalError', 'Cannot find url of luci.js');
 
-                               return rpcClass.declare({
-                                       'object': 'session',
-                                       'method': 'access',
-                                       'params': [ 'scope', 'object', 'function' ],
-                                       'expect': { access: true }
-                               })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
-                       });
+                       env.cgi_base = env.scriptname.replace(/\/[^\/]+$/, '');
 
-                       Request.addInterceptor(function(res) {
-                               var isDenied = false;
+                       Object.assign(this.env, env);
 
-                               if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
-                                       isDenied = true;
+                       document.addEventListener('poll-start', function(ev) {
+                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+                                       e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
+                               });
+                       });
 
-                               if (!isDenied)
-                                       return;
+                       document.addEventListener('poll-stop', function(ev) {
+                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+                                       e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
+                               });
+                       });
 
-                               L.notifySessionExpiry();
+                       var domReady = new Promise(function(resolveFn, rejectFn) {
+                               document.addEventListener('DOMContentLoaded', resolveFn);
                        });
 
-                       return this.probeSystemFeatures().finally(this.initDOM);
-               },
+                       Promise.all([
+                               domReady,
+                               this.require('ui'),
+                               this.require('rpc'),
+                               this.require('form'),
+                               this.probeRPCBaseURL()
+                       ]).then(this.setupDOM.bind(this)).catch(this.error);
 
-               /* private */
-               initDOM: function() {
-                       originalCBIInit();
-                       Poll.start();
-                       document.dispatchEvent(new CustomEvent('luci-loaded'));
+                       originalCBIInit = window.cbi_init;
+                       window.cbi_init = function() {};
                },
 
                /**
-                * The `env` object holds environment settings used by LuCI, such
-                * as request timeouts, base URLs etc.
+                * Captures the current stack trace and throws an error of the
+                * specified type as a new exception. Also logs the exception as
+                * error to the debug console if it is available.
                 *
                 * @instance
                 * @memberof LuCI
-                */
-               env: {},
-
-               /**
-                * Construct a relative URL path from the given prefix and parts.
-                * The resulting URL is guaranteed to only contain the characters
-                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
-                * as `/` for the path separator.
                 *
-                * @instance
-                * @memberof LuCI
+                * @param {Error|string} [type=Error]
+                * Either a string specifying the type of the error to throw or an
+                * existing `Error` instance to copy.
                 *
-                * @param {string} [prefix]
-                * The prefix to join the given parts with. If the `prefix` is
-                * omitted, it defaults to an empty string.
+                * @param {string} [fmt=Unspecified error]
+                * A format string which is used to form the error message, together
+                * with all subsequent optional arguments.
                 *
-                * @param {string[]} [parts]
-                * An array of parts to join into an URL path. Parts may contain
-                * slashes and any of the other characters mentioned above.
+                * @param {...*} [args]
+                * Zero or more variable arguments to the supplied format string.
                 *
-                * @return {string}
-                * Return the joined URL path.
+                * @throws {Error}
+                * Throws the created error object with the captured stack trace
+                * appended to the message and the type set to the given type
+                * argument or copied from the given error instance.
                 */
-               path: function(prefix, parts) {
-                       var url = [ prefix || '' ];
+               raise: function(type, fmt /*, ...*/) {
+                       var e = null,
+                           msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
+                           stack = null;
 
-                       for (var i = 0; i < parts.length; i++)
-                               if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
-                                       url.push('/', parts[i]);
+                       if (type instanceof Error) {
+                               e = type;
 
-                       if (url.length === 1)
-                               url.push('/');
+                               if (msg)
+                                       e.message = msg + ': ' + e.message;
+                       }
+                       else {
+                               try { throw new Error('stacktrace') }
+                               catch (e2) { stack = (e2.stack || '').split(/\n/) }
 
-                       return url.join('');
+                               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 : '';
+                       });
+
+                       if (!/^  at /.test(stack[0]))
+                               stack.shift();
+
+                       if (/\braise /.test(stack[0]))
+                               stack.shift();
+
+                       if (/\berror /.test(stack[0]))
+                               stack.shift();
+
+                       if (stack.length)
+                               e.message += '\n' + stack.join('\n');
+
+                       if (window.console && console.debug)
+                               console.debug(e);
+
+                       throw e;
                },
 
                /**
-                * Construct an URL  pathrelative to the script path of the server
-                * side LuCI application (usually `/cgi-bin/luci`).
-                *
-                * The resulting URL is guaranteed to only contain the characters
-                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
-                * as `/` for the path separator.
+                * A wrapper around {@link LuCI#raise raise()} which also renders
+                * the error either as modal overlay when `ui.js` is already loaed
+                * or directly into the view body.
                 *
                 * @instance
                 * @memberof LuCI
                 *
-                * @param {string[]} [parts]
-                * An array of parts to join into an URL path. Parts may contain
-                * slashes and any of the other characters mentioned above.
+                * @param {Error|string} [type=Error]
+                * Either a string specifying the type of the error to throw or an
+                * existing `Error` instance to copy.
                 *
-                * @return {string}
-                * Returns the resulting URL path.
+                * @param {string} [fmt=Unspecified error]
+                * A format string which is used to form the error message, together
+                * with all subsequent optional arguments.
+                *
+                * @param {...*} [args]
+                * Zero or more variable arguments to the supplied format string.
+                *
+                * @throws {Error}
+                * Throws the created error object with the captured stack trace
+                * appended to the message and the type set to the given type
+                * argument or copied from the given error instance.
                 */
-               url: function() {
-                       return this.path(this.env.scriptname, arguments);
+               error: function(type, fmt /*, ...*/) {
+                       try {
+                               L.raise.apply(L, Array.prototype.slice.call(arguments));
+                       }
+                       catch (e) {
+                               if (!e.reported) {
+                                       if (L.ui)
+                                               L.ui.addNotification(e.name || _('Runtime error'),
+                                                       E('pre', {}, e.message), 'danger');
+                                       else
+                                               DOM.content(document.querySelector('#maincontent'),
+                                                       E('pre', { 'class': 'alert-message error' }, e.message));
+
+                                       e.reported = true;
+                               }
+
+                               throw e;
+                       }
                },
 
                /**
-                * Construct an URL path relative to the global static resource path
-                * of the LuCI ui (usually `/luci-static/resources`).
-                *
-                * The resulting URL is guaranteed to only contain the characters
-                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
-                * as `/` for the path separator.
+                * Return a bound function using the given `self` as `this` context
+                * and any further arguments as parameters to the bound function.
                 *
                 * @instance
                 * @memberof LuCI
                 *
-                * @param {string[]} [parts]
-                * An array of parts to join into an URL path. Parts may contain
-                * slashes and any of the other characters mentioned above.
+                * @param {function} fn
+                * The function to bind.
                 *
-                * @return {string}
-                * Returns the resulting URL path.
+                * @param {*} self
+                * The value to bind as `this` context to the specified function.
+                *
+                * @param {...*} [args]
+                * Zero or more variable arguments which are bound to the function
+                * as parameters.
+                *
+                * @returns {function}
+                * Returns the bound function.
                 */
-               resource: function() {
-                       return this.path(this.env.resource, arguments);
+               bind: function(fn, self /*, ... */) {
+                       return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
                },
 
                /**
-                * Construct an URL path relative to the media resource path of the
-                * LuCI ui (usually `/luci-static/$theme_name`).
-                *
-                * The resulting URL is guaranteed to only contain the characters
-                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
-                * as `/` for the path separator.
+                * Load an additional LuCI JavaScript class and its dependencies,
+                * instantiate it and return the resulting class instance. Each
+                * class is only loaded once. Subsequent attempts to load the same
+                * class will return the already instantiated class.
                 *
                 * @instance
                 * @memberof LuCI
                 *
-                * @param {string[]} [parts]
-                * An array of parts to join into an URL path. Parts may contain
-                * slashes and any of the other characters mentioned above.
-                *
-                * @return {string}
-                * Returns the resulting URL path.
-                */
-               media: function() {
-                       return this.path(this.env.media, arguments);
-               },
-
-               /**
-                * Return the complete URL path to the current view.
-                *
-                * @instance
-                * @memberof LuCI
-                *
-                * @return {string}
-                * Returns the URL path to the current view.
-                */
-               location: function() {
-                       return this.path(this.env.scriptname, this.env.requestpath);
-               },
-
-
-               /**
-                * Tests whether the passed argument is a JavaScript object.
-                * This function is meant to be an object counterpart to the
-                * standard `Array.isArray()` function.
-                *
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {*} [val]
-                * The value to test
-                *
-                * @return {boolean}
-                * Returns `true` if the given value is of type object and
-                * not `null`, else returns `false`.
-                */
-               isObject: function(val) {
-                       return (val != null && typeof(val) == 'object');
-               },
-
-               /**
-                * Return an array of sorted object keys, optionally sorted by
-                * a different key or a different sorting mode.
+                * @param {string} name
+                * The name of the class to load in dotted notation. Dots will
+                * be replaced by spaces and joined with the runtime-determined
+                * base URL of LuCI.js to form an absolute URL to load the class
+                * file from.
                 *
-                * @instance
-                * @memberof LuCI
+                * @throws {DependencyError}
+                * Throws a `DependencyError` when the class to load includes
+                * circular dependencies.
                 *
-                * @param {object} obj
-                * The object to extract the keys from. If the given value is
-                * not an object, the function will return an empty array.
+                * @throws {NetworkError}
+                * Throws `NetworkError` when the underlying {@link LuCI.request}
+                * call failed.
                 *
-                * @param {string} [key]
-                * Specifies the key to order by. This is mainly useful for
-                * nested objects of objects or objects of arrays when sorting
-                * shall not be performed by the primary object keys but by
-                * some other key pointing to a value within the nested values.
+                * @throws {SyntaxError}
+                * Throws `SyntaxError` when the loaded class file code cannot
+                * be interpreted by `eval`.
                 *
-                * @param {string} [sortmode]
-                * May be either `addr` or `num` to override the natural
-                * lexicographic sorting with a sorting suitable for IP/MAC style
-                * addresses or numeric values respectively.
+                * @throws {TypeError}
+                * Throws `TypeError` when the class file could be loaded and
+                * interpreted, but when invoking its code did not yield a valid
+                * class instance.
                 *
-                * @return {string[]}
-                * Returns an array containing the sorted keys of the given object.
+                * @returns {Promise<LuCI.baseclass>}
+                * Returns the instantiated class.
                 */
-               sortedKeys: function(obj, key, sortmode) {
-                       if (obj == null || typeof(obj) != 'object')
-                               return [];
-
-                       return Object.keys(obj).map(function(e) {
-                               var v = (key != null) ? obj[e][key] : e;
-
-                               switch (sortmode) {
-                               case 'addr':
-                                       v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
-                                               function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
-                                       break;
-
-                               case 'num':
-                                       v = (v != null) ? +v : null;
-                                       break;
-                               }
+               require: function(name, from) {
+                       var L = this, url = null, from = from || [];
 
-                               return [ e, v ];
-                       }).filter(function(e) {
-                               return (e[1] != null);
-                       }).sort(function(a, b) {
-                               return (a[1] > b[1]);
-                       }).map(function(e) {
-                               return e[0];
-                       });
-               },
+                       /* Class already loaded */
+                       if (classes[name] != null) {
+                               /* Circular dependency */
+                               if (from.indexOf(name) != -1)
+                                       L.raise('DependencyError',
+                                               'Circular dependency: class "%s" depends on "%s"',
+                                               name, from.join('" which depends on "'));
 
-               /**
-                * Converts the given value to an array. If the given value is of
-                * type array, it is returned as-is, values of type object are
-                * returned as one-element array containing the object, empty
-                * strings and `null` values are returned as empty array, all other
-                * values are converted using `String()`, trimmed, split on white
-                * space and returned as array.
-                *
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {*} val
-                * The value to convert into an array.
-                *
-                * @return {Array<*>}
-                * Returns the resulting array.
-                */
-               toArray: function(val) {
-                       if (val == null)
-                               return [];
-                       else if (Array.isArray(val))
-                               return val;
-                       else if (typeof(val) == 'object')
-                               return [ val ];
+                               return Promise.resolve(classes[name]);
+                       }
 
-                       var s = String(val).trim();
+                       url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
+                       from = [ name ].concat(from);
 
-                       if (s == '')
-                               return [];
+                       var compileClass = function(res) {
+                               if (!res.ok)
+                                       L.raise('NetworkError',
+                                               'HTTP error %d while loading class file "%s"', res.status, url);
 
-                       return s.split(/\s+/);
-               },
+                               var source = res.text(),
+                                   requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
+                                   strictmatch = /^use[ \t]+strict$/,
+                                   depends = [],
+                                   args = '';
 
-               /**
-                * Returns a promise resolving with either the given value or or with
-                * the given default in case the input value is a rejecting promise.
-                *
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {*} value
-                * The value to resolve the promise with.
-                *
-                * @param {*} defvalue
-                * The default value to resolve the promise with in case the given
-                * input value is a rejecting promise.
-                *
-                * @returns {Promise<*>}
-                * 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 });
-               },
+                               /* find require statements in source */
+                               for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
+                                       var chr = source.charCodeAt(i);
 
-               /**
-                * The request callback function is invoked whenever an HTTP
-                * reply to a request made using the `L.get()`, `L.post()` or
-                * `L.poll()` function is timed out or received successfully.
-                *
-                * @instance
-                * @memberof LuCI
-                *
-                * @callback LuCI.requestCallbackFn
-                * @param {XMLHTTPRequest} xhr
-                * The XMLHTTPRequest instance used to make the request.
-                *
-                * @param {*} data
-                * The response JSON if the response could be parsed as such,
-                * else `null`.
-                *
-                * @param {number} duration
-                * The total duration of the request in milliseconds.
-                */
+                                       if (esc) {
+                                               esc = false;
+                                       }
+                                       else if (chr == 92) {
+                                               esc = true;
+                                       }
+                                       else if (chr == quote) {
+                                               var s = source.substring(off, i),
+                                                   m = requirematch.exec(s);
 
-               /**
-                * Issues a GET request to the given url and invokes the specified
-                * callback function. The function is a wrapper around
-                * {@link LuCI.Request#request Request.request()}.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {string} url
-                * The URL to request.
-                *
-                * @param {Object<string, string>} [args]
-                * Additional query string arguments to append to the URL.
-                *
-                * @param {LuCI.requestCallbackFn} cb
-                * The callback function to invoke when the request finishes.
-                *
-                * @return {Promise<null>}
-                * Returns a promise resolving to `null` when concluded.
-                */
-               get: function(url, args, cb) {
-                       return this.poll(null, url, args, cb, false);
-               },
+                                               if (m) {
+                                                       var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
+                                                       depends.push(L.require(dep, from));
+                                                       args += ', ' + as;
+                                               }
+                                               else if (!strictmatch.exec(s)) {
+                                                       break;
+                                               }
 
-               /**
-                * Issues a POST request to the given url and invokes the specified
-                * callback function. The function is a wrapper around
-                * {@link LuCI.Request#request Request.request()}. The request is
-                * sent using `application/x-www-form-urlencoded` encoding and will
-                * contain a field `token` with the current value of `LuCI.env.token`
-                * by default.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {string} url
-                * The URL to request.
-                *
-                * @param {Object<string, string>} [args]
-                * Additional post arguments to append to the request body.
-                *
-                * @param {LuCI.requestCallbackFn} cb
-                * The callback function to invoke when the request finishes.
-                *
-                * @return {Promise<null>}
-                * Returns a promise resolving to `null` when concluded.
-                */
-               post: function(url, args, cb) {
-                       return this.poll(null, url, args, cb, true);
-               },
+                                               off = -1;
+                                               quote = -1;
+                                       }
+                                       else if (quote == -1 && (chr == 34 || chr == 39)) {
+                                               off = i + 1;
+                                               quote = chr;
+                                       }
+                               }
 
-               /**
-                * Register a polling HTTP request that invokes the specified
-                * callback function. The function is a wrapper around
-                * {@link LuCI.Request.poll#add Request.poll.add()}.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {number} interval
-                * The poll interval to use. If set to a value less than or equal
-                * to `0`, it will default to the global poll interval configured
-                * in `LuCI.env.pollinterval`.
-                *
-                * @param {string} url
-                * The URL to request.
-                *
-                * @param {Object<string, string>} [args]
-                * Specifies additional arguments for the request. For GET requests,
-                * the arguments are appended to the URL as query string, for POST
-                * requests, they'll be added to the request body.
-                *
-                * @param {LuCI.requestCallbackFn} cb
-                * The callback function to invoke whenever a request finishes.
-                *
-                * @param {boolean} [post=false]
-                * When set to `false` or not specified, poll requests will be made
-                * using the GET method. When set to `true`, POST requests will be
-                * issued. In case of POST requests, the request body will contain
-                * an argument `token` with the current value of `LuCI.env.token` by
-                * default, regardless of the parameters specified with `args`.
-                *
-                * @return {function}
-                * Returns the internally created function that has been passed to
-                * {@link LuCI.Request.poll#add Request.poll.add()}. This value can
-                * be passed to {@link LuCI.Poll.remove Poll.remove()} to remove the
-                * polling request.
-                */
-               poll: function(interval, url, args, cb, post) {
-                       if (interval !== null && interval <= 0)
-                               interval = this.env.pollinterval;
+                               /* load dependencies and instantiate class */
+                               return Promise.all(depends).then(function(instances) {
+                                       var _factory, _class;
 
-                       var data = post ? { token: this.env.token } : null,
-                           method = post ? 'POST' : 'GET';
+                                       try {
+                                               _factory = eval(
+                                                       '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
+                                                               .format(args, source, res.url));
+                                       }
+                                       catch (error) {
+                                               L.raise('SyntaxError', '%s\n  in %s:%s',
+                                                       error.message, res.url, error.lineNumber || '?');
+                                       }
 
-                       if (!/^(?:\/|\S+:\/\/)/.test(url))
-                               url = this.url(url);
+                                       _factory.displayName = toCamelCase(name + 'ClassFactory');
+                                       _class = _factory.apply(_factory, [window, document, L].concat(instances));
 
-                       if (args != null)
-                               data = Object.assign(data || {}, args);
+                                       if (!Class.isSubclass(_class))
+                                           L.error('TypeError', '"%s" factory yields invalid constructor', name);
 
-                       if (interval !== null)
-                               return Request.poll.add(interval, url, { method: method, query: data }, cb);
-                       else
-                               return Request.request(url, { method: method, query: data })
-                                       .then(function(res) {
-                                               var json = null;
-                                               if (/^application\/json\b/.test(res.headers.get('Content-Type')))
-                                                       try { json = res.json() } catch(e) {}
-                                               cb(res.xhr, json, res.duration);
-                                       });
-               },
+                                       if (_class.displayName == 'AnonymousClass')
+                                               _class.displayName = toCamelCase(name + 'Class');
 
-               /**
-                * Deprecated wrapper around {@link LuCI.Poll.remove Poll.remove()}.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @param {function} entry
-                * The polling function to remove.
-                *
-                * @return {boolean}
-                * Returns `true` when the function has been removed or `false` if
-                * it could not be found.
-                */
-               stop: function(entry) { return Poll.remove(entry) },
+                                       var ptr = Object.getPrototypeOf(L),
+                                           parts = name.split(/\./),
+                                           instance = new _class();
 
-               /**
-                * Deprecated wrapper around {@link LuCI.Poll.stop Poll.stop()}.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @return {boolean}
-                * Returns `true` when the polling loop has been stopped or `false`
-                * when it didn't run to begin with.
-                */
-               halt: function() { return Poll.stop() },
+                                       for (var i = 0; ptr && i < parts.length - 1; i++)
+                                               ptr = ptr[parts[i]];
 
-               /**
-                * Deprecated wrapper around {@link LuCI.Poll.start Poll.start()}.
-                *
-                * @deprecated
-                * @instance
-                * @memberof LuCI
-                *
-                * @return {boolean}
-                * Returns `true` when the polling loop has been started or `false`
-                * when it was already running.
-                */
-               run: function() { return Poll.start() },
+                                       if (ptr)
+                                               ptr[parts[i]] = instance;
 
+                                       classes[name] = instance;
 
-               /**
-                * @class
-                * @memberof LuCI
-                * @hideconstructor
-                * @classdesc
-                *
-                * The `dom` class provides convenience method for creating and
-                * manipulating DOM elements.
-                */
-               dom: Class.singleton(/* @lends LuCI.dom.prototype */ {
-                       __name__: 'LuCI.DOM',
+                                       return instance;
+                               });
+                       };
 
-                       /**
-                        * Tests whether the given argument is a valid DOM `Node`.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} e
-                        * The value to test.
-                        *
-                        * @returns {boolean}
-                        * Returns `true` if the value is a DOM `Node`, else `false`.
-                        */
-                       elem: function(e) {
-                               return (e != null && typeof(e) == 'object' && 'nodeType' in e);
-                       },
+                       /* Request class file */
+                       classes[name] = Request.get(url, { cache: true }).then(compileClass);
 
-                       /**
-                        * Parses a given string as HTML and returns the first child node.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {string} s
-                        * A string containing an HTML fragment to parse. Note that only
-                        * the first result of the resulting structure is returned, so an
-                        * input value of `<div>foo</div> <div>bar</div>` will only return
-                        * the first `div` element node.
-                        *
-                        * @returns {Node}
-                        * 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;
+                       return classes[name];
+               },
 
+               /* DOM setup */
+               probeRPCBaseURL: function() {
+                       if (rpcBaseURL == null) {
                                try {
-                                       domParser = domParser || new DOMParser();
-                                       elem = domParser.parseFromString(s, 'text/html').body.firstChild;
+                                       rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
                                }
-                               catch(e) {}
+                               catch (e) { }
+                       }
+
+                       if (rpcBaseURL == null) {
+                               var rpcFallbackURL = this.url('admin/ubus');
 
-                               if (!elem) {
+                               rpcBaseURL = Request.get(this.env.ubuspath).then(function(res) {
+                                       return (rpcBaseURL = (res.status == 400) ? L.env.ubuspath : rpcFallbackURL);
+                               }, function() {
+                                       return (rpcBaseURL = rpcFallbackURL);
+                               }).then(function(url) {
                                        try {
-                                               dummyElem = dummyElem || document.createElement('div');
-                                               dummyElem.innerHTML = s;
-                                               elem = dummyElem.firstChild;
+                                               window.sessionStorage.setItem('rpcBaseURL', url);
                                        }
-                                       catch (e) {}
-                               }
-
-                               return elem || null;
-                       },
+                                       catch (e) { }
 
-                       /**
-                        * Tests whether a given `Node` matches the given query selector.
-                        *
-                        * This function is a convenience wrapper around the standard
-                        * `Node.matches("selector")` function with the added benefit that
-                        * the `node` argument may be a non-`Node` value, in which case
-                        * this function simply returns `false`.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} node
-                        * The `Node` argument to test the selector against.
-                        *
-                        * @param {string} [selector]
-                        * The query selector expression to test against the given node.
-                        *
-                        * @returns {boolean}
-                        * Returns `true` if the given node matches the specified selector
-                        * 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;
-                               return m ? m.call(node, selector) : false;
-                       },
+                                       return url;
+                               });
+                       }
 
-                       /**
-                        * Returns the closest parent node that matches the given query
-                        * selector expression.
-                        *
-                        * This function is a convenience wrapper around the standard
-                        * `Node.closest("selector")` function with the added benefit that
-                        * the `node` argument may be a non-`Node` value, in which case
-                        * this function simply returns `null`.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} node
-                        * The `Node` argument to find the closest parent for.
-                        *
-                        * @param {string} [selector]
-                        * The query selector expression to test against each parent.
-                        *
-                        * @returns {Node|null}
-                        * Returns the closest parent node matching the selector or
-                        * `null` when the node argument is no valid DOM `Node` or the
-                        * selector didn't match any parent.
-                        */
-                       parent: function(node, selector) {
-                               if (this.elem(node) && node.closest)
-                                       return node.closest(selector);
+                       return Promise.resolve(rpcBaseURL);
+               },
 
-                               while (this.elem(node))
-                                       if (this.matches(node, selector))
-                                               return node;
-                                       else
-                                               node = node.parentNode;
+               probeSystemFeatures: function() {
+                       var sessionid = classes.rpc.getSessionID();
 
-                               return null;
-                       },
+                       if (sysFeatures == null) {
+                               try {
+                                       var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
 
-                       /**
-                        * Appends the given children data to the given node.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} node
-                        * The `Node` argument to append the children to.
-                        *
-                        * @param {*} [children]
-                        * The childrens to append to the given node.
-                        *
-                        * When `children` is an array, then each item of the array
-                        * will be either appended as child element or text node,
-                        * depending on whether the item is a DOM `Node` instance or
-                        * some other non-`null` value. Non-`Node`, non-`null` values
-                        * will be converted to strings first before being passed as
-                        * argument to `createTextNode()`.
-                        *
-                        * When `children` is a function, it will be invoked with
-                        * the passed `node` argument as sole parameter and the `append`
-                        * function will be invoked again, with the given `node` argument
-                        * as first and the return value of the `children` function as
-                        * second parameter.
-                        *
-                        * When `children` is is a DOM `Node` instance, it will be
-                        * appended to the given `node`.
-                        *
-                        * When `children` is any other non-`null` value, it will be
-                        * converted to a string and appened to the `innerHTML` property
-                        * of the given `node`.
-                        *
-                        * @returns {Node|null}
-                        * Returns the last children `Node` appended to the node or `null`
-                        * 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) {
-                               if (!this.elem(node))
-                                       return null;
+                                       if (this.isObject(data) && this.isObject(data[sessionid]))
+                                               sysFeatures = data[sessionid];
+                               }
+                               catch (e) {}
+                       }
 
-                               if (Array.isArray(children)) {
-                                       for (var 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]));
+                       if (!this.isObject(sysFeatures)) {
+                               sysFeatures = classes.rpc.declare({
+                                       object: 'luci',
+                                       method: 'getFeatures',
+                                       expect: { '': {} }
+                               })().then(function(features) {
+                                       try {
+                                               var data = {};
+                                                   data[sessionid] = features;
 
-                                       return node.lastChild;
-                               }
-                               else if (typeof(children) === 'function') {
-                                       return this.append(node, children(node));
-                               }
-                               else if (this.elem(children)) {
-                                       return node.appendChild(children);
-                               }
-                               else if (children !== null && children !== undefined) {
-                                       node.innerHTML = '' + children;
-                                       return node.lastChild;
-                               }
+                                               window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
+                                       }
+                                       catch (e) {}
 
-                               return null;
-                       },
+                                       sysFeatures = features;
 
-                       /**
-                        * Replaces the content of the given node with the given children.
-                        *
-                        * This function first removes any children of the given DOM
-                        * `Node` and then adds the given given children following the
-                        * rules outlined below.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} node
-                        * The `Node` argument to replace the children of.
-                        *
-                        * @param {*} [children]
-                        * The childrens to replace into the given node.
-                        *
-                        * When `children` is an array, then each item of the array
-                        * will be either appended as child element or text node,
-                        * depending on whether the item is a DOM `Node` instance or
-                        * some other non-`null` value. Non-`Node`, non-`null` values
-                        * will be converted to strings first before being passed as
-                        * argument to `createTextNode()`.
-                        *
-                        * When `children` is a function, it will be invoked with
-                        * the passed `node` argument as sole parameter and the `append`
-                        * function will be invoked again, with the given `node` argument
-                        * as first and the return value of the `children` function as
-                        * second parameter.
-                        *
-                        * When `children` is is a DOM `Node` instance, it will be
-                        * appended to the given `node`.
-                        *
-                        * When `children` is any other non-`null` value, it will be
-                        * converted to a string and appened to the `innerHTML` property
-                        * of the given `node`.
-                        *
-                        * @returns {Node|null}
-                        * Returns the last children `Node` appended to the node or `null`
-                        * 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) {
-                               if (!this.elem(node))
-                                       return null;
+                                       return features;
+                               });
+                       }
 
-                               var dataNodes = node.querySelectorAll('[data-idref]');
+                       return Promise.resolve(sysFeatures);
+               },
 
-                               for (var i = 0; i < dataNodes.length; i++)
-                                       delete this.registry[dataNodes[i].getAttribute('data-idref')];
+               /**
+                * Test whether a particular system feature is available, such as
+                * hostapd SAE support or an installed firewall. The features are
+                * queried once at the beginning of the LuCI session and cached in
+                * `SessionStorage` throughout the lifetime of the associated tab or
+                * browser window.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string} feature
+                * The feature to test. For detailed list of known feature flags,
+                * see `/modules/luci-base/root/usr/libexec/rpcd/luci`.
+                *
+                * @param {string} [subfeature]
+                * Some feature classes like `hostapd` provide sub-feature flags,
+                * such as `sae` or `11w` support. The `subfeature` argument can
+                * be used to query these.
+                *
+                * @return {boolean|null}
+                * Return `true` if the queried feature (and sub-feature) is available
+                * or `false` if the requested feature isn't present or known.
+                * Return `null` when a sub-feature was queried for a feature which
+                * has no sub-features.
+                */
+               hasSystemFeature: function() {
+                       var ft = sysFeatures[arguments[0]];
 
-                               while (node.firstChild)
-                                       node.removeChild(node.firstChild);
+                       if (arguments.length == 2)
+                               return this.isObject(ft) ? ft[arguments[1]] : null;
 
-                               return this.append(node, children);
-                       },
+                       return (ft != null && ft != false);
+               },
 
-                       /**
-                        * Sets attributes or registers event listeners on element nodes.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} node
-                        * The `Node` argument to set the attributes or add the event
-                        * listeners for. When the given `node` value is not a valid
-                        * DOM `Node`, the function returns and does nothing.
-                        *
-                        * @param {string|Object<string, *>} key
-                        * Specifies either the attribute or event handler name to use,
-                        * or an object containing multiple key, value pairs which are
-                        * each added to the node as either attribute or event handler,
-                        * depending on the respective value.
-                        *
-                        * @param {*} [val]
-                        * Specifies the attribute value or event handler function to add.
-                        * If the `key` parameter is an `Object`, this parameter will be
-                        * ignored.
-                        *
-                        * When `val` is of type function, it will be registered as event
-                        * handler on the given `node` with the `key` parameter being the
-                        * event name.
-                        *
-                        * When `val` is of type object, it will be serialized as JSON and
-                        * added as attribute to the given `node`, using the given `key`
-                        * as attribute name.
-                        *
-                        * When `val` is of any other type, it will be added as attribute
-                        * to the given `node` as-is, with the underlying `setAttribute()`
-                        * call implicitely turning it into a string.
-                        */
-                       attr: function(node, key, val) {
-                               if (!this.elem(node))
-                                       return null;
+               /* private */
+               notifySessionExpiry: function() {
+                       Poll.stop();
 
-                               var attr = null;
+                       L.ui.showModal(_('Session expired'), [
+                               E('div', { class: 'alert-message warning' },
+                                       _('A new login is required since the authentication session expired.')),
+                               E('div', { class: 'right' },
+                                       E('div', {
+                                               class: 'btn primary',
+                                               click: function() {
+                                                       var loc = window.location;
+                                                       window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
+                                               }
+                                       }, _('To login…')))
+                       ]);
 
-                               if (typeof(key) === 'object' && key !== null)
-                                       attr = key;
-                               else if (typeof(key) === 'string')
-                                       attr = {}, attr[key] = val;
+                       L.raise('SessionError', 'Login session is expired');
+               },
 
-                               for (key in attr) {
-                                       if (!attr.hasOwnProperty(key) || attr[key] == null)
-                                               continue;
+               /* private */
+               setupDOM: function(res) {
+                       var domEv = res[0],
+                           uiClass = res[1],
+                           rpcClass = res[2],
+                           formClass = res[3],
+                           rpcBaseURL = res[4];
 
-                                       switch (typeof(attr[key])) {
-                                       case 'function':
-                                               node.addEventListener(key, attr[key]);
-                                               break;
+                       rpcClass.setBaseURL(rpcBaseURL);
 
-                                       case 'object':
-                                               node.setAttribute(key, JSON.stringify(attr[key]));
-                                               break;
+                       rpcClass.addInterceptor(function(msg, req) {
+                               if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
+                                       return;
 
-                                       default:
-                                               node.setAttribute(key, attr[key]);
-                                       }
-                               }
-                       },
+                               if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+                                       return;
 
-                       /**
-                        * Creates a new DOM `Node` from the given `html`, `attr` and
-                        * `data` parameters.
-                        *
-                        * This function has multiple signatures, it can be either invoked
-                        * in the form `create(html[, attr[, data]])` or in the form
-                        * `create(html[, data])`. The used variant is determined from the
-                        * type of the second argument.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {*} html
-                        * Describes the node to create.
-                        *
-                        * When the value of `html` is of type array, a `DocumentFragment`
-                        * node is created and each item of the array is first converted
-                        * to a DOM `Node` by passing it through `create()` and then added
-                        * as child to the fragment.
-                        *
-                        * When the value of `html` is a DOM `Node` instance, no new
-                        * element will be created but the node will be used as-is.
-                        *
-                        * When the value of `html` is a string starting with `<`, it will
-                        * be passed to `dom.parse()` and the resulting value is used.
-                        *
-                        * When the value of `html` is any other string, it will be passed
-                        * to `document.createElement()` for creating a new DOM `Node` of
-                        * the given name.
-                        *
-                        * @param {Object<string, *>} [attr]
-                        * Specifies an Object of key, value pairs to set as attributes
-                        * or event handlers on the created node. Refer to
-                        * {@link LuCI.dom#attr dom.attr()} for details.
-                        *
-                        * @param {*} [data]
-                        * Specifies children to append to the newly created element.
-                        * Refer to {@link LuCI.dom#append dom.append()} for details.
-                        *
-                        * @throws {InvalidCharacterError}
-                        * Throws an `InvalidCharacterError` when the given `html`
-                        * argument contained malformed markup (such as not escaped
-                        * `&` characters in XHTML mode) or when the given node name
-                        * in `html` contains characters which are not legal in DOM
-                        * element names, such as spaces.
-                        *
-                        * @returns {Node}
-                        * Returns the newly created `Node`.
-                        */
-                       create: function() {
-                               var html = arguments[0],
-                                   attr = arguments[1],
-                                   data = arguments[2],
-                                   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++)
-                                               elem.appendChild(this.create(html[i]));
-                               }
-                               else if (this.elem(html)) {
-                                       elem = html;
-                               }
-                               else if (html.charCodeAt(0) === 60) {
-                                       elem = this.parse(html);
-                               }
-                               else {
-                                       elem = document.createElement(html);
-                               }
+                               return rpcClass.declare({
+                                       'object': 'session',
+                                       'method': 'access',
+                                       'params': [ 'scope', 'object', 'function' ],
+                                       'expect': { access: true }
+                               })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
+                       });
 
-                               if (!elem)
-                                       return null;
+                       Request.addInterceptor(function(res) {
+                               var isDenied = false;
 
-                               this.attr(elem, attr);
-                               this.append(elem, data);
+                               if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
+                                       isDenied = true;
 
-                               return elem;
-                       },
+                               if (!isDenied)
+                                       return;
 
-                       registry: {},
+                               L.notifySessionExpiry();
+                       });
 
-                       /**
-                        * Attaches or detaches arbitrary data to and from a DOM `Node`.
-                        *
-                        * This function is useful to attach non-string values or runtime
-                        * data that is not serializable to DOM nodes. To decouple data
-                        * from the DOM, values are not added directly to nodes, but
-                        * inserted into a registry instead which is then referenced by a
-                        * string key stored as `data-idref` attribute in the node.
-                        *
-                        * This function has multiple signatures and is sensitive to the
-                        * number of arguments passed to it.
-                        *
-                        *  - `dom.data(node)` -
-                        *     Fetches all data associated with the given node.
-                        *  - `dom.data(node, key)` -
-                        *     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.
-                        *  - `dom.data(node, null)` -
-                        *     Clears any data associated with the node.
-                        *  - `dom.data(node, key, null)` -
-                        *     Clears the given key associated with the node.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {Node} node
-                        * The DOM `Node` instance to set or retrieve the data for.
-                        *
-                        * @param {string|null} [key]
-                        * This is either a string specifying the key to retrieve, or
-                        * `null` to unset the entire node data.
-                        *
-                        * @param {*|null} [val]
-                        * This is either a non-`null` value to set for a given key or
-                        * `null` to remove the given `key` from the specified node.
-                        *
-                        * @returns {*}
-                        * Returns the get or set value, or `null` when no value could
-                        * be found.
-                        */
-                       data: function(node, key, val) {
-                               if (!node || !node.getAttribute)
-                                       return null;
+                       return this.probeSystemFeatures().finally(this.initDOM);
+               },
 
-                               var id = node.getAttribute('data-idref');
+               /* private */
+               initDOM: function() {
+                       originalCBIInit();
+                       Poll.start();
+                       document.dispatchEvent(new CustomEvent('luci-loaded'));
+               },
 
-                               /* clear all data */
-                               if (arguments.length > 1 && key == null) {
-                                       if (id != null) {
-                                               node.removeAttribute('data-idref');
-                                               val = this.registry[id]
-                                               delete this.registry[id];
-                                               return val;
-                                       }
+               /**
+                * The `env` object holds environment settings used by LuCI, such
+                * as request timeouts, base URLs etc.
+                *
+                * @instance
+                * @memberof LuCI
+                */
+               env: {},
 
-                                       return null;
-                               }
+               /**
+                * Construct a relative URL path from the given prefix and parts.
+                * The resulting URL is guaranteed to only contain the characters
+                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
+                * as `/` for the path separator.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string} [prefix]
+                * The prefix to join the given parts with. If the `prefix` is
+                * omitted, it defaults to an empty string.
+                *
+                * @param {string[]} [parts]
+                * An array of parts to join into an URL path. Parts may contain
+                * slashes and any of the other characters mentioned above.
+                *
+                * @return {string}
+                * Return the joined URL path.
+                */
+               path: function(prefix, parts) {
+                       var url = [ prefix || '' ];
 
-                               /* clear a key */
-                               else if (arguments.length > 2 && key != null && val == null) {
-                                       if (id != null) {
-                                               val = this.registry[id][key];
-                                               delete this.registry[id][key];
-                                               return val;
-                                       }
+                       for (var i = 0; i < parts.length; i++)
+                               if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
+                                       url.push('/', parts[i]);
 
-                                       return null;
-                               }
+                       if (url.length === 1)
+                               url.push('/');
 
-                               /* set a key */
-                               else if (arguments.length > 2 && key != null && val != null) {
-                                       if (id == null) {
-                                               do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
-                                               while (this.registry.hasOwnProperty(id));
+                       return url.join('');
+               },
 
-                                               node.setAttribute('data-idref', id);
-                                               this.registry[id] = {};
-                                       }
+               /**
+                * Construct an URL  pathrelative to the script path of the server
+                * side LuCI application (usually `/cgi-bin/luci`).
+                *
+                * The resulting URL is guaranteed to only contain the characters
+                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
+                * as `/` for the path separator.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string[]} [parts]
+                * An array of parts to join into an URL path. Parts may contain
+                * slashes and any of the other characters mentioned above.
+                *
+                * @return {string}
+                * Returns the resulting URL path.
+                */
+               url: function() {
+                       return this.path(this.env.scriptname, arguments);
+               },
 
-                                       return (this.registry[id][key] = val);
-                               }
+               /**
+                * Construct an URL path relative to the global static resource path
+                * of the LuCI ui (usually `/luci-static/resources`).
+                *
+                * The resulting URL is guaranteed to only contain the characters
+                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
+                * as `/` for the path separator.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string[]} [parts]
+                * An array of parts to join into an URL path. Parts may contain
+                * slashes and any of the other characters mentioned above.
+                *
+                * @return {string}
+                * Returns the resulting URL path.
+                */
+               resource: function() {
+                       return this.path(this.env.resource, arguments);
+               },
 
-                               /* get all data */
-                               else if (arguments.length == 1) {
-                                       if (id != null)
-                                               return this.registry[id];
+               /**
+                * Construct an URL path relative to the media resource path of the
+                * LuCI ui (usually `/luci-static/$theme_name`).
+                *
+                * The resulting URL is guaranteed to only contain the characters
+                * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
+                * as `/` for the path separator.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string[]} [parts]
+                * An array of parts to join into an URL path. Parts may contain
+                * slashes and any of the other characters mentioned above.
+                *
+                * @return {string}
+                * Returns the resulting URL path.
+                */
+               media: function() {
+                       return this.path(this.env.media, arguments);
+               },
 
-                                       return null;
-                               }
+               /**
+                * Return the complete URL path to the current view.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @return {string}
+                * Returns the URL path to the current view.
+                */
+               location: function() {
+                       return this.path(this.env.scriptname, this.env.requestpath);
+               },
 
-                               /* get a key */
-                               else if (arguments.length == 2) {
-                                       if (id != null)
-                                               return this.registry[id][key];
-                               }
 
-                               return null;
-                       },
+               /**
+                * Tests whether the passed argument is a JavaScript object.
+                * This function is meant to be an object counterpart to the
+                * standard `Array.isArray()` function.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {*} [val]
+                * The value to test
+                *
+                * @return {boolean}
+                * Returns `true` if the given value is of type object and
+                * not `null`, else returns `false`.
+                */
+               isObject: function(val) {
+                       return (val != null && typeof(val) == 'object');
+               },
 
-                       /**
-                        * Binds the given class instance ot the specified DOM `Node`.
-                        *
-                        * This function uses the `dom.data()` facility to attach the
-                        * passed instance of a Class to a node. This is needed for
-                        * complex widget elements or similar where the corresponding
-                        * class instance responsible for the element must be retrieved
-                        * from DOM nodes obtained by `querySelector()` or similar means.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {Node} node
-                        * The DOM `Node` instance to bind the class to.
-                        *
-                        * @param {Class} inst
-                        * The Class instance to bind to the node.
-                        *
-                        * @throws {TypeError}
-                        * Throws a `TypeError` when the given instance argument isn't
-                        * a valid Class instance.
-                        *
-                        * @returns {Class}
-                        * Returns the bound class instance.
-                        */
-                       bindClassInstance: function(node, inst) {
-                               if (!(inst instanceof Class))
-                                       L.error('TypeError', 'Argument must be a class instance');
+               /**
+                * Return an array of sorted object keys, optionally sorted by
+                * a different key or a different sorting mode.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {object} obj
+                * The object to extract the keys from. If the given value is
+                * not an object, the function will return an empty array.
+                *
+                * @param {string} [key]
+                * Specifies the key to order by. This is mainly useful for
+                * nested objects of objects or objects of arrays when sorting
+                * shall not be performed by the primary object keys but by
+                * some other key pointing to a value within the nested values.
+                *
+                * @param {string} [sortmode]
+                * May be either `addr` or `num` to override the natural
+                * lexicographic sorting with a sorting suitable for IP/MAC style
+                * addresses or numeric values respectively.
+                *
+                * @return {string[]}
+                * Returns an array containing the sorted keys of the given object.
+                */
+               sortedKeys: function(obj, key, sortmode) {
+                       if (obj == null || typeof(obj) != 'object')
+                               return [];
 
-                               return this.data(node, '_class', inst);
-                       },
+                       return Object.keys(obj).map(function(e) {
+                               var v = (key != null) ? obj[e][key] : e;
 
-                       /**
-                        * Finds a bound class instance on the given node itself or the
-                        * first bound instance on its closest parent node.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {Node} node
-                        * The DOM `Node` instance to start from.
-                        *
-                        * @returns {Class|null}
-                        * 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;
+                               switch (sortmode) {
+                               case 'addr':
+                                       v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
+                                               function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
+                                       break;
 
-                               do {
-                                       inst = this.data(node, '_class');
-                                       node = node.parentNode;
+                               case 'num':
+                                       v = (v != null) ? +v : null;
+                                       break;
                                }
-                               while (!(inst instanceof Class) && node != null);
-
-                               return inst;
-                       },
-
-                       /**
-                        * Finds a bound class instance on the given node itself or the
-                        * first bound instance on its closest parent node and invokes
-                        * the specified method name on the found class instance.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {Node} node
-                        * The DOM `Node` instance to start from.
-                        *
-                        * @param {string} method
-                        * The name of the method to invoke on the found class instance.
-                        *
-                        * @param {...*} params
-                        * Additional arguments to pass to the invoked method as-is.
-                        *
-                        * @returns {*|null}
-                        * Returns the return value of the invoked method if a class
-                        * instance and method has been found. Returns `null` if either
-                        * 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);
 
-                               if (inst == null || typeof(inst[method]) != 'function')
-                                       return null;
+                               return [ e, v ];
+                       }).filter(function(e) {
+                               return (e[1] != null);
+                       }).sort(function(a, b) {
+                               return (a[1] > b[1]);
+                       }).map(function(e) {
+                               return e[0];
+                       });
+               },
 
-                               return inst[method].apply(inst, inst.varargs(arguments, 2));
-                       },
+               /**
+                * Converts the given value to an array. If the given value is of
+                * type array, it is returned as-is, values of type object are
+                * returned as one-element array containing the object, empty
+                * strings and `null` values are returned as empty array, all other
+                * values are converted using `String()`, trimmed, split on white
+                * space and returned as array.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {*} val
+                * The value to convert into an array.
+                *
+                * @return {Array<*>}
+                * Returns the resulting array.
+                */
+               toArray: function(val) {
+                       if (val == null)
+                               return [];
+                       else if (Array.isArray(val))
+                               return val;
+                       else if (typeof(val) == 'object')
+                               return [ val ];
 
-                       /**
-                        * The ignore callback function is invoked by `isEmpty()` for each
-                        * child node to decide whether to ignore a child node or not.
-                        *
-                        * When this function returns `false`, the node passed to it is
-                        * ignored, else not.
-                        *
-                        * @callback LuCI.dom~ignoreCallbackFn
-                        * @param {Node} node
-                        * The child node to test.
-                        *
-                        * @returns {boolean}
-                        * Boolean indicating whether to ignore the node or not.
-                        */
+                       var s = String(val).trim();
 
-                       /**
-                        * Tests whether a given DOM `Node` instance is empty or appears
-                        * empty.
-                        *
-                        * Any element child nodes which have the CSS class `hidden` set
-                        * or for which the optionally passed `ignoreFn` callback function
-                        * returns `false` are ignored.
-                        *
-                        * @instance
-                        * @memberof LuCI.dom
-                        * @param {Node} node
-                        * The DOM `Node` instance to test.
-                        *
-                        * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
-                        * Specifies an optional function which is invoked for each child
-                        * node to decide whether the child node should be ignored or not.
-                        *
-                        * @returns {boolean}
-                        * Returns `true` if the node does not have any children or if
-                        * 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)))
-                                               return false;
+                       if (s == '')
+                               return [];
 
-                               return true;
-                       }
-               }),
+                       return s.split(/\s+/);
+               },
 
-               Poll: Poll,
-               Class: Class,
-               Request: Request,
+               /**
+                * Returns a promise resolving with either the given value or or with
+                * the given default in case the input value is a rejecting promise.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {*} value
+                * The value to resolve the promise with.
+                *
+                * @param {*} defvalue
+                * The default value to resolve the promise with in case the given
+                * input value is a rejecting promise.
+                *
+                * @returns {Promise<*>}
+                * 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 });
+               },
 
                /**
-                * @class
+                * The request callback function is invoked whenever an HTTP
+                * reply to a request made using the `L.get()`, `L.post()` or
+                * `L.poll()` function is timed out or received successfully.
+                *
+                * @instance
                 * @memberof LuCI
-                * @hideconstructor
-                * @classdesc
                 *
-                * The `view` class forms the basis of views and provides a standard
-                * set of methods to inherit from.
+                * @callback LuCI.requestCallbackFn
+                * @param {XMLHTTPRequest} xhr
+                * The XMLHTTPRequest instance used to make the request.
+                *
+                * @param {*} data
+                * The response JSON if the response could be parsed as such,
+                * else `null`.
+                *
+                * @param {number} duration
+                * The total duration of the request in milliseconds.
                 */
-               view: Class.extend(/* @lends LuCI.view.prototype */ {
-                       __name__: 'LuCI.View',
 
-                       __init__: function() {
-                               var vp = document.getElementById('view');
-
-                               L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
+               /**
+                * Issues a GET request to the given url and invokes the specified
+                * callback function. The function is a wrapper around
+                * {@link LuCI.request#request Request.request()}.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string} url
+                * The URL to request.
+                *
+                * @param {Object<string, string>} [args]
+                * Additional query string arguments to append to the URL.
+                *
+                * @param {LuCI.requestCallbackFn} cb
+                * The callback function to invoke when the request finishes.
+                *
+                * @return {Promise<null>}
+                * Returns a promise resolving to `null` when concluded.
+                */
+               get: function(url, args, cb) {
+                       return this.poll(null, url, args, cb, false);
+               },
 
-                               return Promise.resolve(this.load())
-                                       .then(L.bind(this.render, this))
-                                       .then(L.bind(function(nodes) {
-                                               var vp = document.getElementById('view');
+               /**
+                * Issues a POST request to the given url and invokes the specified
+                * callback function. The function is a wrapper around
+                * {@link LuCI.request#request Request.request()}. The request is
+                * sent using `application/x-www-form-urlencoded` encoding and will
+                * contain a field `token` with the current value of `LuCI.env.token`
+                * by default.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {string} url
+                * The URL to request.
+                *
+                * @param {Object<string, string>} [args]
+                * Additional post arguments to append to the request body.
+                *
+                * @param {LuCI.requestCallbackFn} cb
+                * The callback function to invoke when the request finishes.
+                *
+                * @return {Promise<null>}
+                * Returns a promise resolving to `null` when concluded.
+                */
+               post: function(url, args, cb) {
+                       return this.poll(null, url, args, cb, true);
+               },
 
-                                               L.dom.content(vp, nodes);
-                                               L.dom.append(vp, this.addFooter());
-                                       }, this)).catch(L.error);
-                       },
+               /**
+                * Register a polling HTTP request that invokes the specified
+                * callback function. The function is a wrapper around
+                * {@link LuCI.request.poll#add Request.poll.add()}.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {number} interval
+                * The poll interval to use. If set to a value less than or equal
+                * to `0`, it will default to the global poll interval configured
+                * in `LuCI.env.pollinterval`.
+                *
+                * @param {string} url
+                * The URL to request.
+                *
+                * @param {Object<string, string>} [args]
+                * Specifies additional arguments for the request. For GET requests,
+                * the arguments are appended to the URL as query string, for POST
+                * requests, they'll be added to the request body.
+                *
+                * @param {LuCI.requestCallbackFn} cb
+                * The callback function to invoke whenever a request finishes.
+                *
+                * @param {boolean} [post=false]
+                * When set to `false` or not specified, poll requests will be made
+                * using the GET method. When set to `true`, POST requests will be
+                * issued. In case of POST requests, the request body will contain
+                * an argument `token` with the current value of `LuCI.env.token` by
+                * default, regardless of the parameters specified with `args`.
+                *
+                * @return {function}
+                * Returns the internally created function that has been passed to
+                * {@link LuCI.request.poll#add Request.poll.add()}. This value can
+                * be passed to {@link LuCI.poll.remove Poll.remove()} to remove the
+                * polling request.
+                */
+               poll: function(interval, url, args, cb, post) {
+                       if (interval !== null && interval <= 0)
+                               interval = this.env.pollinterval;
 
-                       /**
-                        * The load function is invoked before the view is rendered.
-                        *
-                        * The invocation of this function is wrapped by
-                        * `Promise.resolve()` so it may return Promises if needed.
-                        *
-                        * The return value of the function (or the resolved values
-                        * of the promise returned by it) will be passed as first
-                        * argument to `render()`.
-                        *
-                        * This function is supposed to be overwritten by subclasses,
-                        * the default implementation does nothing.
-                        *
-                        * @instance
-                        * @abstract
-                        * @memberof LuCI.view
-                        *
-                        * @returns {*|Promise<*>}
-                        * May return any value or a Promise resolving to any value.
-                        */
-                       load: function() {},
+                       var data = post ? { token: this.env.token } : null,
+                           method = post ? 'POST' : 'GET';
 
-                       /**
-                        * The render function is invoked after the
-                        * {@link LuCI.view#load load()} function and responsible
-                        * for setting up the view contents. It must return a DOM
-                        * `Node` or `DocumentFragment` holding the contents to
-                        * insert into the view area.
-                        *
-                        * The invocation of this function is wrapped by
-                        * `Promise.resolve()` so it may return Promises if needed.
-                        *
-                        * The return value of the function (or the resolved values
-                        * of the promise returned by it) will be inserted into the
-                        * main content area using
-                        * {@link LuCI.dom#append dom.append()}.
-                        *
-                        * This function is supposed to be overwritten by subclasses,
-                        * the default implementation does nothing.
-                        *
-                        * @instance
-                        * @abstract
-                        * @memberof LuCI.view
-                        * @param {*|null} load_results
-                        * This function will receive the return value of the
-                        * {@link LuCI.view#load view.load()} function as first
-                        * argument.
-                        *
-                        * @returns {Node|Promise<Node>}
-                        * Should return a DOM `Node` value or a `Promise` resolving
-                        * to a `Node` value.
-                        */
-                       render: function() {},
+                       if (!/^(?:\/|\S+:\/\/)/.test(url))
+                               url = this.url(url);
 
-                       /**
-                        * The handleSave function is invoked when the user clicks
-                        * the `Save` button in the page action footer.
-                        *
-                        * The default implementation should be sufficient for most
-                        * views using {@link form#Map form.Map()} based forms - it
-                        * will iterate all forms present in the view and invoke
-                        * the {@link form#Map#save Map.save()} method on each form.
-                        *
-                        * Views not using `Map` instances or requiring other special
-                        * logic should overwrite `handleSave()` with a custom
-                        * implementation.
-                        *
-                        * To disable the `Save` page footer button, views extending
-                        * this base class should overwrite the `handleSave` function
-                        * with `null`.
-                        *
-                        * The invocation of this function is wrapped by
-                        * `Promise.resolve()` so it may return Promises if needed.
-                        *
-                        * @instance
-                        * @memberof LuCI.view
-                        * @param {Event} ev
-                        * The DOM event that triggered the function.
-                        *
-                        * @returns {*|Promise<*>}
-                        * Any return values of this function are discarded, but
-                        * passed through `Promise.resolve()` to ensure that any
-                        * returned promise runs to completion before the button
-                        * is reenabled.
-                        */
-                       handleSave: function(ev) {
-                               var tasks = [];
+                       if (args != null)
+                               data = Object.assign(data || {}, args);
 
-                               document.getElementById('maincontent')
-                                       .querySelectorAll('.cbi-map').forEach(function(map) {
-                                               tasks.push(L.dom.callClassMethod(map, 'save'));
+                       if (interval !== null)
+                               return Request.poll.add(interval, url, { method: method, query: data }, cb);
+                       else
+                               return Request.request(url, { method: method, query: data })
+                                       .then(function(res) {
+                                               var json = null;
+                                               if (/^application\/json\b/.test(res.headers.get('Content-Type')))
+                                                       try { json = res.json() } catch(e) {}
+                                               cb(res.xhr, json, res.duration);
                                        });
+               },
 
-                               return Promise.all(tasks);
-                       },
+               /**
+                * Deprecated wrapper around {@link LuCI.poll.remove Poll.remove()}.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {function} entry
+                * The polling function to remove.
+                *
+                * @return {boolean}
+                * Returns `true` when the function has been removed or `false` if
+                * it could not be found.
+                */
+               stop: function(entry) { return Poll.remove(entry) },
 
-                       /**
-                        * The handleSaveApply function is invoked when the user clicks
-                        * the `Save & Apply` button in the page action footer.
-                        *
-                        * The default implementation should be sufficient for most
-                        * views using {@link form#Map form.Map()} based forms - it
-                        * will first invoke
-                        * {@link LuCI.view.handleSave view.handleSave()} and then
-                        * call {@link ui#changes#apply ui.changes.apply()} to start the
-                        * modal config apply and page reload flow.
-                        *
-                        * Views not using `Map` instances or requiring other special
-                        * logic should overwrite `handleSaveApply()` with a custom
-                        * implementation.
-                        *
-                        * To disable the `Save & Apply` page footer button, views
-                        * extending this base class should overwrite the
-                        * `handleSaveApply` function with `null`.
-                        *
-                        * The invocation of this function is wrapped by
-                        * `Promise.resolve()` so it may return Promises if needed.
-                        *
-                        * @instance
-                        * @memberof LuCI.view
-                        * @param {Event} ev
-                        * The DOM event that triggered the function.
-                        *
-                        * @returns {*|Promise<*>}
-                        * Any return values of this function are discarded, but
-                        * passed through `Promise.resolve()` to ensure that any
-                        * returned promise runs to completion before the button
-                        * is reenabled.
-                        */
-                       handleSaveApply: function(ev, mode) {
-                               return this.handleSave(ev).then(function() {
-                                       L.ui.changes.apply(mode == '0');
-                               });
-                       },
+               /**
+                * Deprecated wrapper around {@link LuCI.poll.stop Poll.stop()}.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @return {boolean}
+                * Returns `true` when the polling loop has been stopped or `false`
+                * when it didn't run to begin with.
+                */
+               halt: function() { return Poll.stop() },
 
-                       /**
-                        * The handleReset function is invoked when the user clicks
-                        * the `Reset` button in the page action footer.
-                        *
-                        * The default implementation should be sufficient for most
-                        * views using {@link form#Map form.Map()} based forms - it
-                        * will iterate all forms present in the view and invoke
-                        * the {@link form#Map#save Map.reset()} method on each form.
-                        *
-                        * Views not using `Map` instances or requiring other special
-                        * logic should overwrite `handleReset()` with a custom
-                        * implementation.
-                        *
-                        * To disable the `Reset` page footer button, views extending
-                        * this base class should overwrite the `handleReset` function
-                        * with `null`.
-                        *
-                        * The invocation of this function is wrapped by
-                        * `Promise.resolve()` so it may return Promises if needed.
-                        *
-                        * @instance
-                        * @memberof LuCI.view
-                        * @param {Event} ev
-                        * The DOM event that triggered the function.
-                        *
-                        * @returns {*|Promise<*>}
-                        * Any return values of this function are discarded, but
-                        * passed through `Promise.resolve()` to ensure that any
-                        * returned promise runs to completion before the button
-                        * is reenabled.
-                        */
-                       handleReset: function(ev) {
-                               var tasks = [];
+               /**
+                * Deprecated wrapper around {@link LuCI.poll.start Poll.start()}.
+                *
+                * @deprecated
+                * @instance
+                * @memberof LuCI
+                *
+                * @return {boolean}
+                * Returns `true` when the polling loop has been started or `false`
+                * when it was already running.
+                */
+               run: function() { return Poll.start() },
 
-                               document.getElementById('maincontent')
-                                       .querySelectorAll('.cbi-map').forEach(function(map) {
-                                               tasks.push(L.dom.callClassMethod(map, 'reset'));
-                                       });
+               /**
+                * Legacy `L.dom` class alias. New view code should use `'require dom';`
+                * to request the `LuCI.dom` class.
+                *
+                * @instance
+                * @memberof LuCI
+                * @deprecated
+                */
+               dom: DOM,
 
-                               return Promise.all(tasks);
-                       },
+               /**
+                * Legacy `L.view` class alias. New view code should use `'require view';`
+                * to request the `LuCI.view` class.
+                *
+                * @instance
+                * @memberof LuCI
+                * @deprecated
+                */
+               view: View,
 
-                       /**
-                        * Renders a standard page action footer if any of the
-                        * `handleSave()`, `handleSaveApply()` or `handleReset()`
-                        * functions are defined.
-                        *
-                        * The default implementation should be sufficient for most
-                        * views - it will render a standard page footer with action
-                        * buttons labeled `Save`, `Save & Apply` and `Reset`
-                        * triggering the `handleSave()`, `handleSaveApply()` and
-                        * `handleReset()` functions respectively.
-                        *
-                        * When any of these `handle*()` functions is overwritten
-                        * with `null` by a view extending this class, the
-                        * corresponding button will not be rendered.
-                        *
-                        * @instance
-                        * @memberof LuCI.view
-                        * @returns {DocumentFragment}
-                        * Returns a `DocumentFragment` containing the footer bar
-                        * with buttons for each corresponding `handle*()` action
-                        * or an empty `DocumentFragment` if all three `handle*()`
-                        * methods are overwritten with `null`.
-                        */
-                       addFooter: function() {
-                               var footer = E([]);
-
-                               var saveApplyBtn = this.handleSaveApply ? new L.ui.ComboButton('0', {
-                                       0: [ _('Save & Apply') ],
-                                       1: [ _('Apply unchecked') ]
-                               }, {
-                                       classes: {
-                                               0: 'btn cbi-button cbi-button-apply important',
-                                               1: 'btn cbi-button cbi-button-negative important'
-                                       },
-                                       click: L.ui.createHandlerFn(this, 'handleSaveApply')
-                               }).render() : E([]);
-
-                               if (this.handleSaveApply || this.handleSave || this.handleReset) {
-                                       footer.appendChild(E('div', { 'class': 'cbi-page-actions control-group' }, [
-                                               saveApplyBtn, ' ',
-                                               this.handleSave ? E('button', {
-                                                       'class': 'cbi-button cbi-button-save',
-                                                       'click': L.ui.createHandlerFn(this, 'handleSave')
-                                               }, [ _('Save') ]) : '', ' ',
-                                               this.handleReset ? E('button', {
-                                                       'class': 'cbi-button cbi-button-reset',
-                                                       'click': L.ui.createHandlerFn(this, 'handleReset')
-                                               }, [ _('Reset') ]) : ''
-                                       ]));
-                               }
+               /**
+                * Legacy `L.Poll` class alias. New view code should use `'require poll';`
+                * to request the `LuCI.poll` class.
+                *
+                * @instance
+                * @memberof LuCI
+                * @deprecated
+                */
+               Poll: Poll,
 
-                               return footer;
-                       }
-               })
+               /**
+                * Legacy `L.Request` class alias. New view code should use `'require request';`
+                * to request the `LuCI.request` class.
+                *
+                * @instance
+                * @memberof LuCI
+                * @deprecated
+                */
+               Request: Request,
+
+               /**
+                * Legacy `L.Class` class alias. New view code should use `'require baseclass';`
+                * to request the `LuCI.baseclass` class.
+                *
+                * @instance
+                * @memberof LuCI
+                * @deprecated
+                */
+               Class: Class
        });
 
        /**
-        * @class
+        * @class xhr
         * @memberof LuCI
         * @deprecated
         * @classdesc
         *
-        * The `LuCI.XHR` class is a legacy compatibility shim for the
+        * The `LuCI.xhr` class is a legacy compatibility shim for the
         * functionality formerly provided by `xhr.js`. It is registered as global
         * `window.XHR` symbol for compatibility with legacy code.
         *
-        * New code should use {@link LuCI.Request} instead to implement HTTP
+        * New code should use {@link LuCI.request} instead to implement HTTP
         * request handling.
         */
-       var XHR = Class.extend(/** @lends LuCI.XHR.prototype */ {
-               __name__: 'LuCI.XHR',
+       var XHR = Class.extend(/** @lends LuCI.xhr.prototype */ {
+               __name__: 'LuCI.xhr',
                __init__: function() {
                        if (window.console && console.debug)
                                console.debug('Direct use XHR() is deprecated, please use L.Request instead');
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 *
                 * @param {string} url
                 * The URL to request
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 *
                 * @param {string} url
                 * The URL to request
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 */
                cancel: function() { delete this.active },
 
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 *
                 * @returns {boolean}
                 * Returns `true` if the request is still running or `false` if it
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 */
                abort: function() {},
 
                 *
                 * @instance
                 * @deprecated
-                * @memberof LuCI.XHR
+                * @memberof LuCI.xhr
                 *
                 * @throws {InternalError}
                 * Throws an `InternalError` with the message `Not implemented`
index ec40e78bebcfd45a26164443afa123796d1f2d57..34a802fdfdb0fc7660949e24ba48d180adf2af71 100644 (file)
@@ -2,6 +2,8 @@
 'require uci';
 'require rpc';
 'require validation';
+'require baseclass';
+'require firewall';
 
 var proto_errors = {
        CONNECT_FAILED:                 _('Connection attempt failed'),
@@ -632,18 +634,18 @@ function enumerateNetworks() {
 var Hosts, Network, Protocol, Device, WifiDevice, WifiNetwork;
 
 /**
- * @class
+ * @class network
  * @memberof LuCI
  * @hideconstructor
  * @classdesc
  *
- * The `LuCI.Network` class combines data from multiple `ubus` apis to
+ * The `LuCI.network` class combines data from multiple `ubus` apis to
  * provide an abstraction of the current network configuration state.
  *
  * It provides methods to enumerate interfaces and devices, to query
  * current configuration details and to manipulate settings.
  */
-Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
+Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
        /**
         * Converts the given prefix size in bits to a netmask.
         *
@@ -686,8 +688,8 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * such as the used key management protocols, active ciphers and
         * protocol versions.
         *
-        * @typedef {Object<string, boolean|Array<number|string>>} LuCI.Network.WifiEncryption
-        * @memberof LuCI.Network
+        * @typedef {Object<string, boolean|Array<number|string>>} LuCI.network.WifiEncryption
+        * @memberof LuCI.network
         *
         * @property {boolean} enabled
         * Specifies whether any kind of encryption, such as `WEP` or `WPA` is
@@ -721,13 +723,13 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         */
 
        /**
-        * Converts a given {@link LuCI.Network.WifiEncryption encryption entry}
+        * Converts a given {@link LuCI.network.WifiEncryption encryption entry}
         * into a human readable string such as `mixed WPA/WPA2 PSK (TKIP, CCMP)`
         * or `WPA3 SAE (CCMP)`.
         *
         * @method
         *
-        * @param {LuCI.Network.WifiEncryption} encryption
+        * @param {LuCI.network.WifiEncryption} encryption
         * The wireless encryption entry to convert.
         *
         * @returns {null|string}
@@ -749,7 +751,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Instantiates the given {@link LuCI.Network.Protocol Protocol} backend,
+        * Instantiates the given {@link LuCI.network.Protocol Protocol} backend,
         * optionally using the given network name.
         *
         * @param {string} protoname
@@ -761,7 +763,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * but it is allowed to omit it, e.g. to query protocol capabilities
         * without the need for an existing interface.
         *
-        * @returns {null|LuCI.Network.Protocol}
+        * @returns {null|LuCI.network.Protocol}
         * Returns the instantiated protocol backend class or `null` if the given
         * protocol isn't known.
         */
@@ -774,10 +776,10 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Obtains instances of all known {@link LuCI.Network.Protocol Protocol}
+        * Obtains instances of all known {@link LuCI.network.Protocol Protocol}
         * backend classes.
         *
-        * @returns {Array<LuCI.Network.Protocol>}
+        * @returns {Array<LuCI.network.Protocol>}
         * Returns an array of protocol class instances.
         */
        getProtocols: function() {
@@ -790,7 +792,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Registers a new {@link LuCI.Network.Protocol Protocol} subclass
+        * Registers a new {@link LuCI.network.Protocol Protocol} subclass
         * with the given methods and returns the resulting subclass value.
         *
         * This functions internally calls
@@ -804,7 +806,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * The member methods and values of the new `Protocol` subclass to
         * be passed to {@link LuCI.Class.extend Class.extend()}.
         *
-        * @returns {LuCI.Network.Protocol}
+        * @returns {LuCI.network.Protocol}
         * Returns the new `Protocol` subclass.
         */
        registerProtocol: function(protoname, methods) {
@@ -893,7 +895,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * An object of uci option values to set on the new network or to
         * update in an existing, empty network.
         *
-        * @returns {Promise<null|LuCI.Network.Protocol>}
+        * @returns {Promise<null|LuCI.network.Protocol>}
         * Returns a promise resolving to the `Protocol` subclass instance
         * describing the added network or resolving to `null` if the name
         * was invalid or if a non-empty network of the given name already
@@ -925,15 +927,15 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Get a {@link LuCI.Network.Protocol Protocol} instance describing
+        * Get a {@link LuCI.network.Protocol Protocol} instance describing
         * the network with the given name.
         *
         * @param {string} name
         * The logical interface name of the network get, e.g. `lan` or `wan`.
         *
-        * @returns {Promise<null|LuCI.Network.Protocol>}
+        * @returns {Promise<null|LuCI.network.Protocol>}
         * Returns a promise resolving to a
-        * {@link LuCI.Network.Protocol Protocol} subclass instance describing
+        * {@link LuCI.network.Protocol Protocol} subclass instance describing
         * the network or `null` if the network did not exist.
         */
        getNetwork: function(name) {
@@ -956,9 +958,9 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        /**
         * Gets an array containing all known networks.
         *
-        * @returns {Promise<Array<LuCI.Network.Protocol>>}
+        * @returns {Promise<Array<LuCI.network.Protocol>>}
         * Returns a promise resolving to a name-sorted array of
-        * {@link LuCI.Network.Protocol Protocol} subclass instances
+        * {@link LuCI.network.Protocol Protocol} subclass instances
         * describing all known networks.
         */
        getNetworks: function() {
@@ -981,8 +983,9 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
                var requireFirewall = Promise.resolve(L.require('firewall')).catch(function() {}),
                    network = this.instantiateNetwork(name);
 
-               return Promise.all([ requireFirewall, initNetworkState() ]).then(function() {
-                       var uciInterface = uci.get('network', name);
+               return Promise.all([ requireFirewall, initNetworkState() ]).then(function(res) {
+                       var uciInterface = uci.get('network', name),
+                           firewall = res[0];
 
                        if (uciInterface != null && uciInterface['.type'] == 'interface') {
                                return Promise.resolve(network ? network.deleteConfiguration() : null).then(function() {
@@ -1017,8 +1020,8 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
                                                        uci.unset('wireless', s['.name'], 'network');
                                        });
 
-                                       if (L.firewall)
-                                               return L.firewall.deleteNetwork(name).then(function() { return true });
+                                       if (firewall)
+                                               return firewall.deleteNetwork(name).then(function() { return true });
 
                                        return true;
                                }).catch(function() {
@@ -1096,13 +1099,13 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Get a {@link LuCI.Network.Device Device} instance describing the
+        * Get a {@link LuCI.network.Device Device} instance describing the
         * given network device.
         *
         * @param {string} name
         * The name of the network device to get, e.g. `eth0` or `br-lan`.
         *
-        * @returns {Promise<null|LuCI.Network.Device>}
+        * @returns {Promise<null|LuCI.network.Device>}
         * Returns a promise resolving to the `Device` instance describing
         * the network device or `null` if the given device name could not
         * be found.
@@ -1126,7 +1129,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        /**
         * Get a sorted list of all found network devices.
         *
-        * @returns {Promise<Array<LuCI.Network.Device>>}
+        * @returns {Promise<Array<LuCI.network.Device>>}
         * Returns a promise resolving to a sorted array of `Device` class
         * instances describing the network devices found on the system.
         */
@@ -1251,14 +1254,14 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Get a {@link LuCI.Network.WifiDevice WifiDevice} instance describing
+        * Get a {@link LuCI.network.WifiDevice WifiDevice} instance describing
         * the given wireless radio.
         *
         * @param {string} devname
         * The configuration name of the wireless radio to lookup, e.g. `radio0`
         * for the first mac80211 phy on the system.
         *
-        * @returns {Promise<null|LuCI.Network.WifiDevice>}
+        * @returns {Promise<null|LuCI.network.WifiDevice>}
         * Returns a promise resolving to the `WifiDevice` instance describing
         * the underlying radio device or `null` if the wireless radio could not
         * be found.
@@ -1277,7 +1280,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        /**
         * Obtain a list of all configured radio devices.
         *
-        * @returns {Promise<Array<LuCI.Network.WifiDevice>>}
+        * @returns {Promise<Array<LuCI.network.WifiDevice>>}
         * Returns a promise resolving to an array of `WifiDevice` instances
         * describing the wireless radios configured in the system.
         * The order of the array corresponds to the order of the radios in
@@ -1298,7 +1301,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Get a {@link LuCI.Network.WifiNetwork WifiNetwork} instance describing
+        * Get a {@link LuCI.network.WifiNetwork WifiNetwork} instance describing
         * the given wireless network.
         *
         * @param {string} netname
@@ -1307,7 +1310,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * or a Linux network device name like `wlan0` which is resolved to the
         * corresponding configuration section through `ubus` runtime information.
         *
-        * @returns {Promise<null|LuCI.Network.WifiNetwork>}
+        * @returns {Promise<null|LuCI.network.WifiNetwork>}
         * Returns a promise resolving to the `WifiNetwork` instance describing
         * the wireless network or `null` if the corresponding network could not
         * be found.
@@ -1318,10 +1321,10 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        },
 
        /**
-        * Get an array of all {@link LuCI.Network.WifiNetwork WifiNetwork}
+        * Get an array of all {@link LuCI.network.WifiNetwork WifiNetwork}
         * instances describing the wireless networks present on the system.
         *
-        * @returns {Promise<Array<LuCI.Network.WifiNetwork>>}
+        * @returns {Promise<Array<LuCI.network.WifiNetwork>>}
         * Returns a promise resolving to an array of `WifiNetwork` instances
         * describing the wireless networks. The array will be empty if no networks
         * are found.
@@ -1351,7 +1354,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * must at least contain a `device` property which is set to the radio
         * name the new network belongs to.
         *
-        * @returns {Promise<null|LuCI.Network.WifiNetwork>}
+        * @returns {Promise<null|LuCI.network.WifiNetwork>}
         * Returns a promise resolving to a `WifiNetwork` instance describing
         * the newly added wireless network or `null` if the given options
         * were invalid or if the associated radio device could not be found.
@@ -1472,7 +1475,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * This function looks up all networks having a default `0.0.0.0/0` route
         * and returns them as array.
         *
-        * @returns {Promise<Array<LuCI.Network.Protocol>>}
+        * @returns {Promise<Array<LuCI.network.Protocol>>}
         * Returns a promise resolving to an array of `Protocol` subclass
         * instances describing the found default route interfaces.
         */
@@ -1497,7 +1500,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * This function looks up all networks having a default `::/0` route
         * and returns them as array.
         *
-        * @returns {Promise<Array<LuCI.Network.Protocol>>}
+        * @returns {Promise<Array<LuCI.network.Protocol>>}
         * Returns a promise resolving to an array of `Protocol` subclass
         * instances describing the found IPv6 default route interfaces.
         */
@@ -1521,7 +1524,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         * connections and external port labels of a switch.
         *
         * @typedef {Object<string, Object|Array>} SwitchTopology
-        * @memberof LuCI.Network
+        * @memberof LuCI.network
         *
         * @property {Object<number, string>} netdevs
         * The `netdevs` property points to an object describing the CPU port
@@ -1543,11 +1546,11 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        /**
         * Returns the topologies of all swconfig switches found on the system.
         *
-        * @returns {Promise<Object<string, LuCI.Network.SwitchTopology>>}
+        * @returns {Promise<Object<string, LuCI.network.SwitchTopology>>}
         * Returns a promise resolving to an object containing the topologies
         * of each switch. The object keys correspond to the name of the switches
         * such as `switch0`, the values are
-        * {@link LuCI.Network.SwitchTopology SwitchTopology} objects describing
+        * {@link LuCI.network.SwitchTopology SwitchTopology} objects describing
         * the layout.
         */
        getSwitchTopologies: function() {
@@ -1638,7 +1641,7 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
        /**
         * Obtains the the network device name of the given object.
         *
-        * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} obj
+        * @param {LuCI.network.Protocol|LuCI.network.Device|LuCI.network.WifiDevice|LuCI.network.WifiNetwork|string} obj
         * The object to get the device name from.
         *
         * @returns {null|string}
@@ -1667,10 +1670,10 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
         *
         * This function aggregates information from various sources such as
         * DHCP lease databases, ARP and IPv6 neighbour entries, wireless
-        * association list etc. and returns a {@link LuCI.Network.Hosts Hosts}
+        * association list etc. and returns a {@link LuCI.network.Hosts Hosts}
         * class instance describing the found hosts.
         *
-        * @returns {Promise<LuCI.Network.Hosts>}
+        * @returns {Promise<LuCI.network.Hosts>}
         * Returns a `Hosts` instance describing host known on the system.
         */
        getHostHints: function() {
@@ -1682,15 +1685,15 @@ Network = L.Class.extend(/** @lends LuCI.Network.prototype */ {
 
 /**
  * @class
- * @memberof LuCI.Network
+ * @memberof LuCI.network
  * @hideconstructor
  * @classdesc
  *
- * The `LuCI.Network.Hosts` class encapsulates host information aggregated
+ * The `LuCI.network.Hosts` class encapsulates host information aggregated
  * from multiple sources and provides convenience functions to access the
  * host information by different criteria.
  */
-Hosts = L.Class.extend(/** @lends LuCI.Network.Hosts.prototype */ {
+Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
        __init__: function(hosts) {
                this.hosts = hosts;
        },
@@ -1848,7 +1851,7 @@ Hosts = L.Class.extend(/** @lends LuCI.Network.Hosts.prototype */ {
 
 /**
  * @class
- * @memberof LuCI.Network
+ * @memberof LuCI.network
  * @hideconstructor
  * @classdesc
  *
@@ -1856,7 +1859,7 @@ Hosts = L.Class.extend(/** @lends LuCI.Network.Hosts.prototype */ {
  * subclasses which describe logical UCI networks defined by `config
  * interface` sections in `/etc/config/network`.
  */
-Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
+Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
        __init__: function(name) {
                this.sid = name;
        },
@@ -1933,7 +1936,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         * Get the name of this network protocol class.
         *
         * This function will be overwritten by subclasses created by
-        * {@link LuCI.Network#registerProtocol Network.registerProtocol()}.
+        * {@link LuCI.network#registerProtocol Network.registerProtocol()}.
         *
         * @abstract
         * @returns {string}
@@ -1967,7 +1970,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         * Get the type of the underlying interface.
         *
         * This function actually is a convenience wrapper around
-        * `proto.get("type")` and is mainly used by other `LuCI.Network` code
+        * `proto.get("type")` and is mainly used by other `LuCI.network` code
         * to check whether the interface is declared as bridge in UCI.
         *
         * @returns {null|string}
@@ -2251,7 +2254,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         *
         * This function will translate the found error codes to human readable
         * messages using the descriptions registered by
-        * {@link LuCI.Network#registerErrorCode Network.registerErrorCode()}
+        * {@link LuCI.network#registerErrorCode Network.registerErrorCode()}
         * and fall back to `"Unknown error (%s)"` where `%s` is replaced by the
         * error code in case no translation can be found.
         *
@@ -2302,23 +2305,6 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
                return null;
        },
 
-       /**
-        * Check function for the protocol handler if a new interface is createable.
-        *
-        * This function should be overwritten by protocol specific subclasses.
-        *
-        * @abstract
-        *
-        * @param {string} ifname
-        * The name of the interface to be created.
-        *
-        * @returns {Promise<null|string>}
-        * Returns `null` if new interface is createable, else returns (error) message.
-        */
-       isCreateable: function(ifname) {
-               return Promise.resolve(null);
-       },
-
        /**
         * Checks whether the protocol functionality is installed.
         *
@@ -2452,10 +2438,10 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
        /**
         * Add the given network device to the logical interface.
         *
-        * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device
+        * @param {LuCI.network.Protocol|LuCI.network.Device|LuCI.network.WifiDevice|LuCI.network.WifiNetwork|string} device
         * The object or device name to add to the logical interface. In case the
         * given argument is not a string, it is resolved though the
-        * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function.
+        * {@link LuCI.network#getIfnameOf Network.getIfnameOf()} function.
         *
         * @returns {boolean}
         * Returns `true` if the device name has been added or `false` if any
@@ -2479,10 +2465,10 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
        /**
         * Remove the given network device from the logical interface.
         *
-        * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device
+        * @param {LuCI.network.Protocol|LuCI.network.Device|LuCI.network.WifiDevice|LuCI.network.WifiNetwork|string} device
         * The object or device name to remove from the logical interface. In case
         * the given argument is not a string, it is resolved though the
-        * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function.
+        * {@link LuCI.network#getIfnameOf Network.getIfnameOf()} function.
         *
         * @returns {boolean}
         * Returns `true` if the device name has been added or `false` if any
@@ -2512,7 +2498,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         * Returns the Linux network device associated with this logical
         * interface.
         *
-        * @returns {LuCI.Network.Device}
+        * @returns {LuCI.network.Device}
         * Returns a `Network.Device` class instance representing the
         * expected Linux network device according to the configuration.
         */
@@ -2520,7 +2506,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
                if (this.isVirtual()) {
                        var ifname = '%s-%s'.format(this.getProtocol(), this.sid);
                        _state.isTunnel[this.getProtocol() + '-' + this.sid] = true;
-                       return L.network.instantiateDevice(ifname, this);
+                       return Network.prototype.instantiateDevice(ifname, this);
                }
                else if (this.isBridge()) {
                        var ifname = 'br-%s'.format(this.sid);
@@ -2532,12 +2518,12 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
 
                        for (var i = 0; i < ifnames.length; i++) {
                                var m = ifnames[i].match(/^([^:/]+)/);
-                               return ((m && m[1]) ? L.network.instantiateDevice(m[1], this) : null);
+                               return ((m && m[1]) ? Network.prototype.instantiateDevice(m[1], this) : null);
                        }
 
                        ifname = getWifiNetidByNetname(this.sid);
 
-                       return (ifname != null ? L.network.instantiateDevice(ifname[0], this) : null);
+                       return (ifname != null ? Network.prototype.instantiateDevice(ifname[0], this) : null);
                }
        },
 
@@ -2545,33 +2531,33 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         * Returns the layer 2 linux network device currently associated
         * with this logical interface.
         *
-        * @returns {LuCI.Network.Device}
+        * @returns {LuCI.network.Device}
         * Returns a `Network.Device` class instance representing the Linux
         * network device currently associated with the logical interface.
         */
        getL2Device: function() {
                var ifname = this._ubus('device');
-               return (ifname != null ? L.network.instantiateDevice(ifname, this) : null);
+               return (ifname != null ? Network.prototype.instantiateDevice(ifname, this) : null);
        },
 
        /**
         * Returns the layer 3 linux network device currently associated
         * with this logical interface.
         *
-        * @returns {LuCI.Network.Device}
+        * @returns {LuCI.network.Device}
         * Returns a `Network.Device` class instance representing the Linux
         * network device currently associated with the logical interface.
         */
        getL3Device: function() {
                var ifname = this._ubus('l3_device');
-               return (ifname != null ? L.network.instantiateDevice(ifname, this) : null);
+               return (ifname != null ? Network.prototype.instantiateDevice(ifname, this) : null);
        },
 
        /**
         * Returns a list of network sub-devices associated with this logical
         * interface.
         *
-        * @returns {null|Array<LuCI.Network.Device>}
+        * @returns {null|Array<LuCI.network.Device>}
         * Returns an array of of `Network.Device` class instances representing
         * the sub-devices attached to this logical interface or `null` if the
         * logical interface does not support sub-devices, e.g. because it is
@@ -2591,7 +2577,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
 
                        var m = ifnames[i].match(/^([^:/]+)/);
                        if (m != null)
-                               rv.push(L.network.instantiateDevice(m[1], this));
+                               rv.push(Network.prototype.instantiateDevice(m[1], this));
                }
 
                var uciWifiIfaces = uci.sections('wireless', 'wifi-iface');
@@ -2609,7 +2595,7 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
                                var netid = getWifiNetidBySid(uciWifiIfaces[i]['.name']);
 
                                if (netid != null)
-                                       rv.push(L.network.instantiateDevice(netid[0], this));
+                                       rv.push(Network.prototype.instantiateDevice(netid[0], this));
                        }
                }
 
@@ -2622,10 +2608,10 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
         * Checks whether this logical interface contains the given device
         * object.
         *
-        * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device
+        * @param {LuCI.network.Protocol|LuCI.network.Device|LuCI.network.WifiDevice|LuCI.network.WifiNetwork|string} device
         * The object or device name to check. In case the given argument is not
         * a string, it is resolved though the
-        * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function.
+        * {@link LuCI.network#getIfnameOf Network.getIfnameOf()} function.
         *
         * @returns {boolean}
         * Returns `true` when this logical interface contains the given network
@@ -2684,14 +2670,14 @@ Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ {
 
 /**
  * @class
- * @memberof LuCI.Network
+ * @memberof LuCI.network
  * @hideconstructor
  * @classdesc
  *
  * A `Network.Device` class instance represents an underlying Linux network
  * device and allows querying device details such as packet statistics or MTU.
  */
-Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
+Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
        __init__: function(ifname, network) {
                var wif = getWifiSidByIfname(ifname);
 
@@ -2871,7 +2857,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
        /**
         * Get the associated bridge ports of the device.
         *
-        * @returns {null|Array<LuCI.Network.Device>}
+        * @returns {null|Array<LuCI.network.Device>}
         * Returns an array of `Network.Device` instances representing the ports
         * (slave interfaces) of the bridge or `null` when this device isn't
         * a Linux bridge.
@@ -2884,7 +2870,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
                        return null;
 
                for (var i = 0; i < br.ifnames.length; i++)
-                       rv.push(L.network.instantiateDevice(br.ifnames[i].name));
+                       rv.push(Network.prototype.instantiateDevice(br.ifnames[i].name));
 
                rv.sort(deviceSort);
 
@@ -3000,7 +2986,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
        /**
         * Get the primary logical interface this device is assigned to.
         *
-        * @returns {null|LuCI.Network.Protocol}
+        * @returns {null|LuCI.network.Protocol}
         * Returns a `Network.Protocol` instance representing the logical
         * interface this device is attached to or `null` if it is not
         * assigned to any logical interface.
@@ -3012,7 +2998,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
        /**
         * Get the logical interfaces this device is assigned to.
         *
-        * @returns {Array<LuCI.Network.Protocol>}
+        * @returns {Array<LuCI.network.Protocol>}
         * Returns an array of `Network.Protocol` instances representing the
         * logical interfaces this device is assigned to.
         */
@@ -3035,7 +3021,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
        /**
         * Get the related wireless network this device is related to.
         *
-        * @returns {null|LuCI.Network.WifiNetwork}
+        * @returns {null|LuCI.network.WifiNetwork}
         * Returns a `Network.WifiNetwork` instance representing the wireless
         * network corresponding to this network device or `null` if this device
         * is not a wireless device.
@@ -3047,7 +3033,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
 
 /**
  * @class
- * @memberof LuCI.Network
+ * @memberof LuCI.network
  * @hideconstructor
  * @classdesc
  *
@@ -3055,7 +3041,7 @@ Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ {
  * present on the system and provides wireless capability information as
  * well as methods for enumerating related wireless networks.
  */
-WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
+WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
        __init__: function(name, radiostate) {
                var uciWifiDevice = uci.get('wireless', name);
 
@@ -3207,8 +3193,8 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
         * A wireless scan result object describes a neighbouring wireless
         * network found in the vincinity.
         *
-        * @typedef {Object<string, number|string|LuCI.Network.WifiEncryption>} WifiScanResult
-        * @memberof LuCI.Network
+        * @typedef {Object<string, number|string|LuCI.network.WifiEncryption>} WifiScanResult
+        * @memberof LuCI.network
         *
         * @property {string} ssid
         * The SSID / Mesh ID of the network.
@@ -3233,7 +3219,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
         * The maximum possible quality level of the signal, can be used in
         * conjunction with `quality` to calculate a quality percentage.
         *
-        * @property {LuCI.Network.WifiEncryption} encryption
+        * @property {LuCI.network.WifiEncryption} encryption
         * The encryption used by the wireless network.
         */
 
@@ -3241,7 +3227,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
         * Trigger a wireless scan on this radio device and obtain a list of
         * nearby networks.
         *
-        * @returns {Promise<Array<LuCI.Network.WifiScanResult>>}
+        * @returns {Promise<Array<LuCI.network.WifiScanResult>>}
         * Returns a promise resolving to an array of scan result objects
         * describing the networks found in the vincinity.
         */
@@ -3272,14 +3258,14 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
         * or a Linux network device name like `wlan0` which is resolved to the
         * corresponding configuration section through `ubus` runtime information.
         *
-        * @returns {Promise<LuCI.Network.WifiNetwork>}
+        * @returns {Promise<LuCI.network.WifiNetwork>}
         * Returns a promise resolving to a `Network.WifiNetwork` instance
         * representing the wireless network and rejecting with `null` if
         * the given network could not be found or is not associated with
         * this radio device.
         */
        getWifiNetwork: function(network) {
-               return L.network.getWifiNetwork(network).then(L.bind(function(networkInstance) {
+               return Network.prototype.getWifiNetwork(network).then(L.bind(function(networkInstance) {
                        var uciWifiIface = (networkInstance.sid ? uci.get('wireless', networkInstance.sid) : null);
 
                        if (uciWifiIface == null || uciWifiIface['.type'] != 'wifi-iface' || uciWifiIface.device != this.sid)
@@ -3292,13 +3278,13 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
        /**
         * Get all wireless networks associated with this wireless radio device.
         *
-        * @returns {Promise<Array<LuCI.Network.WifiNetwork>>}
+        * @returns {Promise<Array<LuCI.network.WifiNetwork>>}
         * Returns a promise resolving to an array of `Network.WifiNetwork`
         * instances respresenting the wireless networks associated with this
         * radio device.
         */
        getWifiNetworks: function() {
-               return L.network.getWifiNetworks().then(L.bind(function(networks) {
+               return Network.prototype.getWifiNetworks().then(L.bind(function(networks) {
                        var rv = [];
 
                        for (var i = 0; i < networks.length; i++)
@@ -3316,7 +3302,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
         * @param {Object<string, string|string[]>} [options]
         * The options to set for the newly added wireless network.
         *
-        * @returns {Promise<null|LuCI.Network.WifiNetwork>}
+        * @returns {Promise<null|LuCI.network.WifiNetwork>}
         * Returns a promise resolving to a `WifiNetwork` instance describing
         * the newly added wireless network or `null` if the given options
         * were invalid.
@@ -3327,7 +3313,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
 
                options.device = this.sid;
 
-               return L.network.addWifiNetwork(options);
+               return Network.prototype.addWifiNetwork(options);
        },
 
        /**
@@ -3370,7 +3356,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
 
 /**
  * @class
- * @memberof LuCI.Network
+ * @memberof LuCI.network
  * @hideconstructor
  * @classdesc
  *
@@ -3379,7 +3365,7 @@ WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ {
  * the runtime state of the network. Most radio devices support multiple
  * such networks in parallel.
  */
-WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
+WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */ {
        __init__: function(sid, radioname, radiostate, netid, netstate, hostapd) {
                this.sid    = sid;
                this.netid  = netid;
@@ -3562,7 +3548,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
        /**
         * Get the corresponding wifi radio device.
         *
-        * @returns {null|LuCI.Network.WifiDevice}
+        * @returns {null|LuCI.network.WifiDevice}
         * Returns a `Network.WifiDevice` instance representing the corresponding
         * wifi radio device or `null` if the related radio device could not be
         * found.
@@ -3573,7 +3559,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
                if (radioname == null)
                        return Promise.reject();
 
-               return L.network.getWifiDevice(radioname);
+               return Network.prototype.getWifiDevice(radioname);
        },
 
        /**
@@ -3685,8 +3671,8 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
         * A wireless peer entry describes the properties of a remote wireless
         * peer associated with a local network.
         *
-        * @typedef {Object<string, boolean|number|string|LuCI.Network.WifiRateEntry>} WifiPeerEntry
-        * @memberof LuCI.Network
+        * @typedef {Object<string, boolean|number|string|LuCI.network.WifiRateEntry>} WifiPeerEntry
+        * @memberof LuCI.network
         *
         * @property {string} mac
         * The MAC address (BSSID).
@@ -3784,10 +3770,10 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
         *  - `DEEP SLEEP`
         *  - `UNKNOWN`
         *
-        * @property {LuCI.Network.WifiRateEntry} rx
+        * @property {LuCI.network.WifiRateEntry} rx
         * Describes the receiving wireless rate from the peer.
         *
-        * @property {LuCI.Network.WifiRateEntry} tx
+        * @property {LuCI.network.WifiRateEntry} tx
         * Describes the transmitting wireless rate to the peer.
         */
 
@@ -3796,7 +3782,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
         * transmission rate to or from a peer.
         *
         * @typedef {Object<string, boolean|number>} WifiRateEntry
-        * @memberof LuCI.Network
+        * @memberof LuCI.network
         *
         * @property {number} [drop_misc]
         * The amount of received misc. packages that have been dropped, e.g.
@@ -3853,7 +3839,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
        /**
         * Fetch the list of associated peers.
         *
-        * @returns {Promise<Array<LuCI.Network.WifiPeerEntry>>}
+        * @returns {Promise<Array<LuCI.network.WifiPeerEntry>>}
         * Returns a promise resolving to an array of wireless peers associated
         * with this network.
         */
@@ -4041,7 +4027,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
        /**
         * Get the primary logical interface this wireless network is attached to.
         *
-        * @returns {null|LuCI.Network.Protocol}
+        * @returns {null|LuCI.network.Protocol}
         * Returns a `Network.Protocol` instance representing the logical
         * interface or `null` if this network is not attached to any logical
         * interface.
@@ -4053,7 +4039,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
        /**
         * Get the logical interfaces this wireless network is attached to.
         *
-        * @returns {Array<LuCI.Network.Protocol>}
+        * @returns {Array<LuCI.network.Protocol>}
         * Returns an array of `Network.Protocol` instances representing the
         * logical interfaces this wireless network is attached to.
         */
@@ -4067,7 +4053,7 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
                        if (uciInterface == null || uciInterface['.type'] != 'interface')
                                continue;
 
-                       networks.push(L.network.instantiateNetwork(networkNames[i]));
+                       networks.push(Network.prototype.instantiateNetwork(networkNames[i]));
                }
 
                networks.sort(networkSort);
@@ -4078,12 +4064,12 @@ WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ {
        /**
         * Get the associated Linux network device.
         *
-        * @returns {LuCI.Network.Device}
+        * @returns {LuCI.network.Device}
         * Returns a `Network.Device` instance representing the Linux network
         * device associted with this wireless network.
         */
        getDevice: function() {
-               return L.network.instantiateDevice(this.getIfname());
+               return Network.prototype.instantiateDevice(this.getIfname());
        },
 
        /**
index 9b642444fafac9299efe6298efecfd46e35f1680..20b77c18fc813ec520ca4af2eef6cf3d88e78857 100644 (file)
@@ -1,4 +1,6 @@
 'use strict';
+'require baseclass';
+'require request';
 
 var rpcRequestID = 1,
     rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
@@ -14,7 +16,7 @@ var rpcRequestID = 1,
  * The `LuCI.rpc` class provides high level ubus JSON-RPC abstractions
  * and means for listing and invoking remove RPC methods.
  */
-return L.Class.extend(/** @lends LuCI.rpc.prototype */ {
+return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
        /* privates */
        call: function(req, cb, nobatch) {
                var q = '';
@@ -35,7 +37,7 @@ return L.Class.extend(/** @lends LuCI.rpc.prototype */ {
                        q += '/%s.%s'.format(req.params[1], req.params[2]);
                }
 
-               return L.Request.post(rpcBaseURL + q, req, {
+               return request.post(rpcBaseURL + q, req, {
                        timeout: (L.env.rpctimeout || 20) * 1000,
                        nobatch: nobatch,
                        credentials: true
index 677edf6addb1274d870b21a1b0ed0bc5ae580df8..f381e0b649291191c26ec59379a6989f5efbf9cf 100644 (file)
@@ -1,5 +1,6 @@
 'use strict';
 'require rpc';
+'require baseclass';
 
 /**
  * @class uci
@@ -12,7 +13,7 @@
  * manipulation layer on top to allow for synchroneous operations on
  * UCI configuration data.
  */
-return L.Class.extend(/** @lends LuCI.uci.prototype */ {
+return baseclass.extend(/** @lends LuCI.uci.prototype */ {
        __init__: function() {
                this.state = {
                        newidx:  0,
index 163edb8eaedbb89e5d9bd596e8d41986348cb82b..8d921f77c2324116cb056fdb8e9f97cf9397bb78 100644 (file)
@@ -1,7 +1,11 @@
 'use strict';
+'require validation';
+'require baseclass';
+'require request';
+'require poll';
+'require dom';
 'require rpc';
 'require uci';
-'require validation';
 'require fs';
 
 var modalDiv = null,
@@ -29,7 +33,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 = L.Class.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
+var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
        /**
         * @typedef {Object} InitOptions
         * @memberof LuCI.ui.AbstractElement
@@ -48,7 +52,7 @@ var UIElement = L.Class.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
         * @property {string} [datatype=string]
         * An expression describing the input data validation constraints.
         * It defaults to `string` which will allow any value.
-        * See{@link LuCI.validation} for details on the expression format.
+        * See {@link LuCI.validation} for details on the expression format.
         *
         * @property {function} [validator]
         * Specifies a custom validator function which is invoked after the
@@ -69,7 +73,7 @@ var UIElement = L.Class.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
         * an array of strings or `null` for unset values.
         */
        getValue: function() {
-               if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
+               if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
                        return this.node.value;
 
                return null;
@@ -87,7 +91,7 @@ var UIElement = L.Class.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
         * or `null` values.
         */
        setValue: function(value) {
-               if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
+               if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
                        this.node.value = value;
        },
 
@@ -185,7 +189,7 @@ var UIElement = L.Class.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
                if (!datatype && !validate)
                        return;
 
-               this.vfunc = L.ui.addValidator.apply(L.ui, [
+               this.vfunc = UI.prototype.addValidator.apply(UI.prototype, [
                        targetNode, datatype || 'string',
                        optional, validate
                ].concat(events));
@@ -347,7 +351,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
                this.setUpdateEvents(inputEl, 'keyup', 'blur');
                this.setChangeEvents(inputEl, 'change');
 
-               L.dom.bindClassInstance(frameEl, this);
+               dom.bindClassInstance(frameEl, this);
 
                return frameEl;
        },
@@ -463,7 +467,7 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
                this.setUpdateEvents(inputEl, 'keyup', 'blur');
                this.setChangeEvents(inputEl, 'change');
 
-               L.dom.bindClassInstance(frameEl, this);
+               dom.bindClassInstance(frameEl, this);
 
                return frameEl;
        },
@@ -568,7 +572,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
                this.setUpdateEvents(frameEl.lastElementChild.previousElementSibling, 'click', 'blur');
                this.setChangeEvents(frameEl.lastElementChild.previousElementSibling, 'change');
 
-               L.dom.bindClassInstance(frameEl, this);
+               dom.bindClassInstance(frameEl, this);
 
                return frameEl;
        },
@@ -763,7 +767,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
                        }
                }
 
-               L.dom.bindClassInstance(frameEl, this);
+               dom.bindClassInstance(frameEl, this);
 
                return frameEl;
        },
@@ -929,7 +933,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
         * expression. Only applicable when `create` is `true`.
         */
        __init__: function(value, choices, options) {
-               if (!L.isObject(choices))
+               if (typeof(choices) != 'object')
                        choices = {};
 
                if (!Array.isArray(value))
@@ -976,7 +980,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                for (var i = 0; i < keys.length; i++) {
                        var label = this.choices[keys[i]];
 
-                       if (L.dom.elem(label))
+                       if (dom.elem(label))
                                label = label.cloneNode(true);
 
                        sb.lastElementChild.appendChild(E('li', {
@@ -995,8 +999,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        });
 
                        if (this.options.datatype || this.options.validate)
-                               L.ui.addValidator(createEl, this.options.datatype || 'string',
-                                                 true, this.options.validate, 'blur', 'keyup');
+                               UI.prototype.addValidator(createEl, this.options.datatype || 'string',
+                                                         true, this.options.validate, 'blur', 'keyup');
 
                        sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
                }
@@ -1079,7 +1083,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                else
                        sb.removeAttribute('empty');
 
-               L.dom.content(more, (ndisplay == this.options.display_items)
+               dom.content(more, (ndisplay == this.options.display_items)
                        ? (this.options.select_placeholder || this.options.placeholder) : '···');
 
 
@@ -1118,7 +1122,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
                this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
 
-               L.dom.bindClassInstance(sb, this);
+               dom.bindClassInstance(sb, this);
 
                return sb;
        },
@@ -1343,7 +1347,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        else
                                sb.removeAttribute('empty');
 
-                       L.dom.content(more, (ndisplay === this.options.display_items)
+                       dom.content(more, (ndisplay === this.options.display_items)
                                ? (this.options.select_placeholder || this.options.placeholder) : '···');
                }
                else {
@@ -2017,7 +2021,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
                var sb = ev.currentTarget,
                    t = ev.target;
 
-               if (sb.hasAttribute('open') || L.dom.matches(t, '.cbi-dropdown > span.open'))
+               if (sb.hasAttribute('open') || dom.matches(t, '.cbi-dropdown > span.open'))
                        return UIDropdown.prototype.handleClick.apply(this, arguments);
 
                if (this.options.click)
@@ -2133,14 +2137,14 @@ 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)
-                               L.ui.addValidator(inputEl, this.options.datatype || 'string',
-                                                 true, this.options.validate, 'blur', 'keyup');
+                               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;
 
-                       if (L.dom.elem(label))
+                       if (dom.elem(label))
                                label = label.cloneNode(true);
 
                        this.addItem(dl, this.values[i], label);
@@ -2160,7 +2164,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
                this.setUpdateEvents(dl, 'cbi-dynlist-change');
                this.setChangeEvents(dl, 'cbi-dynlist-change');
 
-               L.dom.bindClassInstance(dl, this);
+               dom.bindClassInstance(dl, this);
 
                return dl;
        },
@@ -2372,7 +2376,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
         */
        addChoices: function(values, labels) {
                var dl = this.node.lastElementChild.firstElementChild;
-               L.dom.callClassMethod(dl, 'addChoices', values, labels);
+               dom.callClassMethod(dl, 'addChoices', values, labels);
        },
 
        /**
@@ -2385,7 +2389,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
         */
        clearChoices: function() {
                var dl = this.node.lastElementChild.firstElementChild;
-               L.dom.callClassMethod(dl, 'clearChoices');
+               dom.callClassMethod(dl, 'clearChoices');
        }
 });
 
@@ -2439,7 +2443,7 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
        bind: function(hiddenEl) {
                this.node = hiddenEl;
 
-               L.dom.bindClassInstance(hiddenEl, this);
+               dom.bindClassInstance(hiddenEl, this);
 
                return hiddenEl;
        },
@@ -2535,7 +2539,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
                this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
 
-               L.dom.bindClassInstance(browserEl, this);
+               dom.bindClassInstance(browserEl, this);
 
                return browserEl;
        },
@@ -2558,7 +2562,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                        return this.bind(E('div', { 'id': this.options.id }, [
                                E('button', {
                                        'class': 'btn',
-                                       'click': L.ui.createHandlerFn(this, 'handleFileBrowser')
+                                       'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser')
                                }, label),
                                E('div', {
                                        'class': 'cbi-filebrowser'
@@ -2657,7 +2661,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                data.append('filename', path + '/' + filename);
                data.append('filedata', fileinput.files[0]);
 
-               return L.Request.post(L.env.cgi_base + '/cgi-upload', data, {
+               return request.post(L.env.cgi_base + '/cgi-upload', data, {
                        progress: L.bind(function(btn, ev) {
                                btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
                        }, this, ev.target)
@@ -2689,7 +2693,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                            hidden = this.node.lastElementChild;
 
                        if (path == hidden.value) {
-                               L.dom.content(button, _('Select file…'));
+                               dom.content(button, _('Select file…'));
                                hidden.value = '';
                        }
 
@@ -2741,7 +2745,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
                                E('button', {
                                        'class': 'btn cbi-button-save',
-                                       'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
+                                       'click': UI.prototype.createHandlerFn(this, 'handleUpload', path, list),
                                        'disabled': true
                                }, [ _('Upload file') ])
                        ])
@@ -2778,7 +2782,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                        E('a', {
                                                'href': '#',
                                                'style': selected ? 'font-weight:bold' : null,
-                                               'click': L.ui.createHandlerFn(this, 'handleSelect',
+                                               'click': UI.prototype.createHandlerFn(this, 'handleSelect',
                                                        entrypath, list[i].type != 'directory' ? list[i] : null)
                                        }, '%h'.format(list[i].name))
                                ]),
@@ -2794,11 +2798,11 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                E('div', [
                                        selected ? E('button', {
                                                'class': 'btn',
-                                               'click': L.ui.createHandlerFn(this, 'handleReset')
+                                               'click': UI.prototype.createHandlerFn(this, 'handleReset')
                                        }, [ _('Deselect') ]) : '',
                                        this.options.enable_remove ? E('button', {
                                                'class': 'btn cbi-button-negative',
-                                               'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
+                                               'click': UI.prototype.createHandlerFn(this, 'handleDelete', entrypath, list[i])
                                        }, [ _('Delete') ]) : ''
                                ])
                        ]));
@@ -2812,16 +2816,16 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
 
                for (var i = 0; i < dirs.length; i++) {
                        cur = cur ? cur + '/' + dirs[i] : dirs[i];
-                       L.dom.append(breadcrumb, [
+                       dom.append(breadcrumb, [
                                i ? ' » ' : '',
                                E('a', {
                                        'href': '#',
-                                       'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
+                                       'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur || '/', null)
                                }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
                        ]);
                }
 
-               L.dom.content(container, [
+               dom.content(container, [
                        breadcrumb,
                        rows,
                        E('div', { 'class': 'right' }, [
@@ -2829,7 +2833,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                                E('a', {
                                        'href': '#',
                                        'class': 'btn',
-                                       'click': L.ui.createHandlerFn(this, 'handleCancel')
+                                       'click': UI.prototype.createHandlerFn(this, 'handleCancel')
                                }, _('Cancel'))
                        ]),
                ]);
@@ -2854,18 +2858,18 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
                    hidden = this.node.lastElementChild;
 
                hidden.value = '';
-               L.dom.content(button, _('Select file…'));
+               dom.content(button, _('Select file…'));
 
                this.handleCancel(ev);
        },
 
        /** @private */
        handleSelect: function(path, fileStat, ev) {
-               var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
+               var browser = dom.parent(ev.target, '.cbi-filebrowser'),
                    ul = browser.querySelector('ul');
 
                if (fileStat == null) {
-                       L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
+                       dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
                        L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
                }
                else {
@@ -2874,7 +2878,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
 
                        path = this.canonicalizePath(path);
 
-                       L.dom.content(button, [
+                       dom.content(button, [
                                this.iconForType(fileStat.type),
                                ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
                        ]);
@@ -2901,7 +2905,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
 
                return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
                        document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
-                               L.dom.findClassInstance(browserEl).handleCancel(ev);
+                               dom.findClassInstance(browserEl).handleCancel(ev);
                        });
 
                        button.style.display = 'none';
@@ -2932,14 +2936,14 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
  * To import the class in views, use `'require ui'`, to import it in
  * external JavaScript, use `L.require("ui").then(...)`.
  */
-return L.Class.extend(/** @lends LuCI.ui.prototype */ {
+var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
        __init__: function() {
                modalDiv = document.body.appendChild(
-                       L.dom.create('div', { id: 'modal_overlay' },
-                               L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
+                       dom.create('div', { id: 'modal_overlay' },
+                               dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
 
                tooltipDiv = document.body.appendChild(
-                       L.dom.create('div', { class: 'cbi-tooltip' }));
+                       dom.create('div', { class: 'cbi-tooltip' }));
 
                /* setup old aliases */
                L.showModal = this.showModal;
@@ -2977,7 +2981,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
         * @param {*} contents
         * The contents to add to the modal dialog. This should be a DOM node or
         * a document fragment in most cases. The value is passed as-is to the
-        * `L.dom.content()` function - refer to its documentation for applicable
+        * `dom.content()` function - refer to its documentation for applicable
         * values.
         *
         * @param {...string} [classes]
@@ -2995,8 +2999,8 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                for (var i = 2; i < arguments.length; i++)
                        dlg.classList.add(arguments[i]);
 
-               L.dom.content(dlg, L.dom.create('h4', {}, title));
-               L.dom.append(dlg, children);
+               dom.content(dlg, dom.create('h4', {}, title));
+               dom.append(dlg, children);
 
                document.body.classList.add('modal-overlay-active');
 
@@ -3092,7 +3096,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
         * @param {*} contents
         * The contents to add to the notification banner. This should be a DOM
         * node or a document fragment in most cases. The value is passed as-is
-        * to the `L.dom.content()` function - refer to its documentation for
+        * to the `dom.content()` function - refer to its documentation for
         * applicable values.
         *
         * @param {...string} [classes]
@@ -3119,7 +3123,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                        'class': 'btn',
                                        'style': 'margin-left:auto; margin-top:auto',
                                        'click': function(ev) {
-                                               L.dom.parent(ev.target, '.alert-message').classList.add('fade-out');
+                                               dom.parent(ev.target, '.alert-message').classList.add('fade-out');
                                        },
 
                                }, [ _('Dismiss') ])
@@ -3127,9 +3131,9 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                ]);
 
                if (title != null)
-                       L.dom.append(msg.firstElementChild, E('h4', {}, title));
+                       dom.append(msg.firstElementChild, E('h4', {}, title));
 
-               L.dom.append(msg.firstElementChild, children);
+               dom.append(msg.firstElementChild, children);
 
                for (var i = 2; i < arguments.length; i++)
                        msg.classList.add(arguments[i]);
@@ -3271,11 +3275,11 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                ]));
 
                                if ((i+2) < items.length)
-                                       children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
+                                       children.push(dom.elem(sep) ? sep.cloneNode(true) : sep);
                        }
                }
 
-               L.dom.content(node, children);
+               dom.content(node, children);
 
                return node;
        },
@@ -3295,7 +3299,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
         * external JavaScript, use `L.require("ui").then(...)` and access the
         * `tabs` property of the class instance value.
         */
-       tabs: L.Class.singleton(/* @lends LuCI.ui.tabs.prototype */ {
+       tabs: baseclass.singleton(/* @lends LuCI.ui.tabs.prototype */ {
                /** @private */
                init: function() {
                        var groups = [], prevGroup = null, currGroup = null;
@@ -3303,7 +3307,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        document.querySelectorAll('[data-tab]').forEach(function(tab) {
                                var parent = tab.parentNode;
 
-                               if (L.dom.matches(tab, 'li') && L.dom.matches(parent, 'ul.cbi-tabmenu'))
+                               if (dom.matches(tab, 'li') && dom.matches(parent, 'ul.cbi-tabmenu'))
                                        return;
 
                                if (!parent.hasAttribute('data-tab-group'))
@@ -3421,7 +3425,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                 * Returns `true` if the pane is empty, else `false`.
                 */
                isEmptyPane: function(pane) {
-                       return L.dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
+                       return dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
                },
 
                /** @private */
@@ -3530,11 +3534,11 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        });
 
                        group.childNodes.forEach(function(pane) {
-                               if (L.dom.matches(pane, '[data-tab]')) {
+                               if (dom.matches(pane, '[data-tab]')) {
                                        if (pane.getAttribute('data-tab') === name) {
                                                pane.setAttribute('data-tab-active', 'true');
                                                pane.dispatchEvent(new CustomEvent('cbi-tab-active', { detail: { tab: name } }));
-                                               L.ui.tabs.setActiveTabId(pane, index);
+                                               UI.prototype.tabs.setActiveTabId(pane, index);
                                        }
                                        else {
                                                pane.setAttribute('data-tab-active', 'false');
@@ -3576,7 +3580,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
         */
        uploadFile: function(path, progressStatusNode) {
                return new Promise(function(resolveFn, rejectFn) {
-                       L.ui.showModal(_('Uploading file…'), [
+                       UI.prototype.showModal(_('Uploading file…'), [
                                E('p', _('Please select the file to upload.')),
                                E('div', { 'style': 'display:flex' }, [
                                        E('div', { 'class': 'left', 'style': 'flex:1' }, [
@@ -3584,7 +3588,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                        type: 'file',
                                                        style: 'display:none',
                                                        change: function(ev) {
-                                                               var modal = L.dom.parent(ev.target, '.modal'),
+                                                               var modal = dom.parent(ev.target, '.modal'),
                                                                    body = modal.querySelector('p'),
                                                                    upload = modal.querySelector('.cbi-button-action.important'),
                                                                    file = ev.currentTarget.files[0];
@@ -3592,7 +3596,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                                if (file == null)
                                                                        return;
 
-                                                               L.dom.content(body, [
+                                                               dom.content(body, [
                                                                        E('ul', {}, [
                                                                                E('li', {}, [ '%s: %s'.format(_('Name'), file.name.replace(/^.*[\\\/]/, '')) ]),
                                                                                E('li', {}, [ '%s: %1024mB'.format(_('Size'), file.size) ])
@@ -3614,7 +3618,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                E('button', {
                                                        'class': 'btn',
                                                        'click': function() {
-                                                               L.ui.hideModal();
+                                                               UI.prototype.hideModal();
                                                                rejectFn(new Error('Upload has been cancelled'));
                                                        }
                                                }, [ _('Cancel') ]),
@@ -3623,14 +3627,14 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                        'class': 'btn cbi-button-action important',
                                                        'disabled': true,
                                                        'click': function(ev) {
-                                                               var input = L.dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
+                                                               var 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' }));
 
-                                                               L.ui.showModal(_('Uploading file…'), [ progress ]);
+                                                               UI.prototype.showModal(_('Uploading file…'), [ progress ]);
 
                                                                var data = new FormData();
 
@@ -3640,7 +3644,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
 
                                                                var filename = input.files[0].name;
 
-                                                               L.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;
@@ -3654,10 +3658,10 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                                }).then(function(res) {
                                                                        var reply = res.json();
 
-                                                                       L.ui.hideModal();
+                                                                       UI.prototype.hideModal();
 
                                                                        if (L.isObject(reply) && reply.failure) {
-                                                                               L.ui.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
+                                                                               UI.prototype.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
                                                                                rejectFn(new Error(reply.failure));
                                                                        }
                                                                        else {
@@ -3665,7 +3669,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                                                                resolveFn(reply);
                                                                        }
                                                                }, function(err) {
-                                                                       L.ui.hideModal();
+                                                                       UI.prototype.hideModal();
                                                                        rejectFn(err);
                                                                });
                                                        }
@@ -3726,7 +3730,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                var ipaddrs = arguments.length ? arguments : [ window.location.host ];
 
                window.setTimeout(L.bind(function() {
-                       L.Poll.add(L.bind(function() {
+                       poll.add(L.bind(function() {
                                var tasks = [], reachable = false;
 
                                for (var i = 0; i < 2; i++)
@@ -3736,7 +3740,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
 
                                return Promise.all(tasks).then(function() {
                                        if (reachable) {
-                                               L.Poll.stop();
+                                               poll.stop();
                                                window.location = reachable;
                                        }
                                });
@@ -3758,7 +3762,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
         * external JavaScript, use `L.require("ui").then(...)` and access the
         * `changes` property of the class instance value.
         */
-       changes: L.Class.singleton(/* @lends LuCI.ui.changes.prototype */ {
+       changes: baseclass.singleton(/* @lends LuCI.ui.changes.prototype */ {
                init: function() {
                        if (!L.env.sessionid)
                                return;
@@ -3791,7 +3795,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        }
 
                        if (n > 0) {
-                               L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
+                               dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
                                i.classList.add('flash');
                                i.style.display = '';
                                document.dispatchEvent(new CustomEvent('uci-new-changes'));
@@ -3850,7 +3854,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                 */
                displayChanges: function() {
                        var list = E('div', { 'class': 'uci-change-list' }),
-                           dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
+                           dlg = UI.prototype.showModal(_('Configuration') + ' / ' + _('Changes'), [
                                E('div', { 'class': 'cbi-section' }, [
                                        E('strong', _('Legend:')),
                                        E('div', { 'class': 'uci-change-legend' }, [
@@ -3866,7 +3870,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                                        E('div', { 'class': 'right' }, [
                                                E('button', {
                                                        'class': 'btn',
-                                                       'click': L.ui.hideModal
+                                                       'click': UI.prototype.hideModal
                                                }, [ _('Dismiss') ]), ' ',
                                                E('button', {
                                                        'class': 'cbi-button cbi-button-positive important',
@@ -3919,24 +3923,24 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                /** @private */
                displayStatus: function(type, content) {
                        if (type) {
-                               var message = L.ui.showModal('', '');
+                               var message = UI.prototype.showModal('', '');
 
                                message.classList.add('alert-message');
                                DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
 
                                if (content)
-                                       L.dom.content(message, content);
+                                       dom.content(message, content);
 
                                if (!this.was_polling) {
-                                       this.was_polling = L.Request.poll.active();
-                                       L.Request.poll.stop();
+                                       this.was_polling = request.poll.active();
+                                       request.poll.stop();
                                }
                        }
                        else {
-                               L.ui.hideModal();
+                               UI.prototype.hideModal();
 
                                if (this.was_polling)
-                                       L.Request.poll.start();
+                                       request.poll.start();
                        }
                },
 
@@ -3949,21 +3953,21 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
 
                                var call = function(r, data, duration) {
                                        if (r.status === 204) {
-                                               L.ui.changes.displayStatus('warning', [
+                                               UI.prototype.changes.displayStatus('warning', [
                                                        E('h4', _('Configuration changes have been rolled back!')),
                                                        E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
                                                        E('div', { 'class': 'right' }, [
                                                                E('button', {
                                                                        'class': 'btn',
-                                                                       'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
+                                                                       'click': L.bind(UI.prototype.changes.displayStatus, UI.prototype.changes, false)
                                                                }, [ _('Dismiss') ]), ' ',
                                                                E('button', {
                                                                        'class': 'btn cbi-button-action important',
-                                                                       'click': L.bind(L.ui.changes.revert, L.ui.changes)
+                                                                       'click': L.bind(UI.prototype.changes.revert, UI.prototype.changes)
                                                                }, [ _('Revert changes') ]), ' ',
                                                                E('button', {
                                                                        'class': 'btn cbi-button-negative important',
-                                                                       'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
+                                                                       'click': L.bind(UI.prototype.changes.apply, UI.prototype.changes, false)
                                                                }, [ _('Apply unchecked') ])
                                                        ])
                                                ]);
@@ -3973,7 +3977,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
 
                                        var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
                                        window.setTimeout(function() {
-                                               L.Request.request(L.url('admin/uci/confirm'), {
+                                               request.request(L.url('admin/uci/confirm'), {
                                                        method: 'post',
                                                        timeout: L.env.apply_timeout * 1000,
                                                        query: { sid: L.env.sessionid, token: L.env.token }
@@ -4004,19 +4008,19 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        var call = function(r, data, duration) {
                                if (Date.now() >= deadline) {
                                        window.clearTimeout(tt);
-                                       L.ui.changes.rollback(checked);
+                                       UI.prototype.changes.rollback(checked);
                                        return;
                                }
                                else if (r && (r.status === 200 || r.status === 204)) {
                                        document.dispatchEvent(new CustomEvent('uci-applied'));
 
-                                       L.ui.changes.setIndicator(0);
-                                       L.ui.changes.displayStatus('notice',
+                                       UI.prototype.changes.setIndicator(0);
+                                       UI.prototype.changes.displayStatus('notice',
                                                E('p', _('Configuration changes applied.')));
 
                                        window.clearTimeout(tt);
                                        window.setTimeout(function() {
-                                               //L.ui.changes.displayStatus(false);
+                                               //UI.prototype.changes.displayStatus(false);
                                                window.location = window.location.href.split('#')[0];
                                        }, L.env.apply_display * 1000);
 
@@ -4025,10 +4029,10 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
 
                                var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
                                window.setTimeout(function() {
-                                       L.Request.request(L.url('admin/uci/confirm'), {
+                                       request.request(L.url('admin/uci/confirm'), {
                                                method: 'post',
                                                timeout: L.env.apply_timeout * 1000,
-                                               query: L.ui.changes.confirm_auth
+                                               query: UI.prototype.changes.confirm_auth
                                        }).then(call, call);
                                }, delay);
                        };
@@ -4036,7 +4040,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        var tick = function() {
                                var now = Date.now();
 
-                               L.ui.changes.displayStatus('notice spinning',
+                               UI.prototype.changes.displayStatus('notice spinning',
                                        E('p', _('Applying configuration changes… %ds')
                                                .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
 
@@ -4077,32 +4081,32 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        this.displayStatus('notice spinning',
                                E('p', _('Starting configuration apply…')));
 
-                       L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
+                       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) {
                                if (r.status === (checked ? 200 : 204)) {
                                        var tok = null; try { tok = r.json(); } catch(e) {}
                                        if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
-                                               L.ui.changes.confirm_auth = tok;
+                                               UI.prototype.changes.confirm_auth = tok;
 
-                                       L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
+                                       UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
                                }
                                else if (checked && r.status === 204) {
-                                       L.ui.changes.displayStatus('notice',
+                                       UI.prototype.changes.displayStatus('notice',
                                                E('p', _('There are no changes to apply')));
 
                                        window.setTimeout(function() {
-                                               L.ui.changes.displayStatus(false);
+                                               UI.prototype.changes.displayStatus(false);
                                        }, L.env.apply_display * 1000);
                                }
                                else {
-                                       L.ui.changes.displayStatus('warning',
+                                       UI.prototype.changes.displayStatus('warning',
                                                E('p', _('Apply request failed with status <code>%h</code>')
                                                        .format(r.responseText || r.statusText || r.status)));
 
                                        window.setTimeout(function() {
-                                               L.ui.changes.displayStatus(false);
+                                               UI.prototype.changes.displayStatus(false);
                                        }, L.env.apply_display * 1000);
                                }
                        });
@@ -4124,29 +4128,29 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        this.displayStatus('notice spinning',
                                E('p', _('Reverting configuration…')));
 
-                       L.Request.request(L.url('admin/uci/revert'), {
+                       request.request(L.url('admin/uci/revert'), {
                                method: 'post',
                                query: { sid: L.env.sessionid, token: L.env.token }
                        }).then(function(r) {
                                if (r.status === 200) {
                                        document.dispatchEvent(new CustomEvent('uci-reverted'));
 
-                                       L.ui.changes.setIndicator(0);
-                                       L.ui.changes.displayStatus('notice',
+                                       UI.prototype.changes.setIndicator(0);
+                                       UI.prototype.changes.displayStatus('notice',
                                                E('p', _('Changes have been reverted.')));
 
                                        window.setTimeout(function() {
-                                               //L.ui.changes.displayStatus(false);
+                                               //UI.prototype.changes.displayStatus(false);
                                                window.location = window.location.href.split('#')[0];
                                        }, L.env.apply_display * 1000);
                                }
                                else {
-                                       L.ui.changes.displayStatus('warning',
+                                       UI.prototype.changes.displayStatus('warning',
                                                E('p', _('Revert request failed with status <code>%h</code>')
                                                        .format(r.statusText || r.status)));
 
                                        window.setTimeout(function() {
-                                               L.ui.changes.displayStatus(false);
+                                               UI.prototype.changes.displayStatus(false);
                                        }, L.env.apply_display * 1000);
                                }
                        });
@@ -4197,7 +4201,7 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
                        events.push('blur', 'keyup');
 
                try {
-                       var cbiValidator = L.validation.create(field, type, optional, vfunc),
+                       var cbiValidator = validation.create(field, type, optional, vfunc),
                            validatorFn = cbiValidator.validate.bind(cbiValidator);
 
                        for (var i = 0; i < events.length; i++)
@@ -4278,3 +4282,5 @@ return L.Class.extend(/** @lends LuCI.ui.prototype */ {
        Hiddenfield: UIHiddenfield,
        FileUpload: UIFileUpload
 });
+
+return UI;
index 0544e2f68005833d2ad0b31fabe961c535e1855f..eea837d64ee5c77a53c711ac31035d132f7ab4da 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
+'require baseclass';
 
-var Validator = L.Class.extend({
+var Validator = baseclass.extend({
        __name__: 'Validation',
 
        __init__: function(field, type, optional, vfunc, validatorFactory) {
@@ -81,7 +82,7 @@ var Validator = L.Class.extend({
 
 });
 
-var ValidatorFactory = L.Class.extend({
+var ValidatorFactory = baseclass.extend({
        __name__: 'ValidatorFactory',
 
        create: function(field, type, optional, vfunc) {