diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index 906500eaa5..91c62ca314 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -6,9 +6,68 @@
var modalDiv = null,
tooltipDiv = null,
+ indicatorDiv = null,
tooltipTimeout = null;
-var UIElement = L.Class.extend({
+/**
+ * @class AbstractElement
+ * @memberof LuCI.ui
+ * @hideconstructor
+ * @classdesc
+ *
+ * The `AbstractElement` class serves as abstract base for the different widgets
+ * implemented by `LuCI.ui`. It provides the common logic for getting and
+ * setting values, for checking the validity state and for wiring up required
+ * events.
+ *
+ * UI widget instances are usually not supposed to be created by view code
+ * directly, instead they're implicitely created by `LuCI.form` when
+ * instantiating CBI forms.
+ *
+ * This class is automatically instantiated as part of `LuCI.ui`. To use it
+ * in views, use `'require ui'` and refer to `ui.AbstractElement`. To import
+ * 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 */ {
+ /**
+ * @typedef {Object} InitOptions
+ * @memberof LuCI.ui.AbstractElement
+ *
+ * @property {string} [id]
+ * Specifies the widget ID to use. It will be used as HTML `id` attribute
+ * on the toplevel widget DOM node.
+ *
+ * @property {string} [name]
+ * Specifies the widget name which is set as HTML `name` attribute on the
+ * corresponding `` element.
+ *
+ * @property {boolean} [optional=true]
+ * Specifies whether the input field allows empty values.
+ *
+ * @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.
+ *
+ * @property {function} [validator]
+ * Specifies a custom validator function which is invoked after the
+ * standard validation constraints are checked. The function should return
+ * `true` to accept the given input value. Any other return value type is
+ * converted to a string and treated as validation error message.
+ */
+
+ /**
+ * Read the current value of the input widget.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @returns {string|string[]|null}
+ * The current value of the input element. For simple inputs like text
+ * fields or selects, the return value type will be a - possibly empty -
+ * string. Complex widgets such as `DynamicList` instances may result in
+ * an array of strings or `null` for unset values.
+ */
getValue: function() {
if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
return this.node.value;
@@ -16,15 +75,45 @@ var UIElement = L.Class.extend({
return null;
},
+ /**
+ * Set the current value of the input widget.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @param {string|string[]|null} value
+ * The value to set the input element to. For simple inputs like text
+ * fields or selects, the value should be a - possibly empty - string.
+ * Complex widgets such as `DynamicList` instances may accept string array
+ * or `null` values.
+ */
setValue: function(value) {
if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
this.node.value = value;
},
+ /**
+ * Check whether the current input value is valid.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @returns {boolean}
+ * Returns `true` if the current input value is valid or `false` if it does
+ * not meet the validation constraints.
+ */
isValid: function() {
return (this.validState !== false);
},
+ /**
+ * Force validation of the current input value.
+ *
+ * Usually input validation is automatically triggered by various DOM events
+ * bound to the input widget. In some cases it is required though to manually
+ * trigger validation runs, e.g. when programmatically altering values.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ */
triggerValidation: function() {
if (typeof(this.vfunc) != 'function')
return false;
@@ -36,6 +125,30 @@ var UIElement = L.Class.extend({
return (wasValid != this.isValid());
},
+ /**
+ * Dispatch a custom (synthetic) event in response to received events.
+ *
+ * Sets up event handlers on the given target DOM node for the given event
+ * names that dispatch a custom event of the given type to the widget root
+ * DOM node.
+ *
+ * The primary purpose of this function is to set up a series of custom
+ * uniform standard events such as `widget-update`, `validation-success`,
+ * `validation-failure` etc. which are triggered by various different
+ * widget specific native DOM events.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @param {Node} targetNode
+ * Specifies the DOM node on which the native event listeners should be
+ * registered.
+ *
+ * @param {string} synevent
+ * The name of the custom event to dispatch to the widget root DOM node.
+ *
+ * @param {string[]} events
+ * The native DOM events for which event handlers should be registered.
+ */
registerEvents: function(targetNode, synevent, events) {
var dispatchFn = L.bind(function(ev) {
this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
@@ -45,6 +158,22 @@ var UIElement = L.Class.extend({
targetNode.addEventListener(events[i], dispatchFn);
},
+ /**
+ * Setup listeners for native DOM events that may update the widget value.
+ *
+ * Sets up event handlers on the given target DOM node for the given event
+ * names which may cause the input value to update, such as `keyup` or
+ * `onclick` events. In contrast to change events, such update events will
+ * trigger input value validation.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @param {Node} targetNode
+ * Specifies the DOM node on which the event listeners should be registered.
+ *
+ * @param {...string} events
+ * The DOM events for which event handlers should be registered.
+ */
setUpdateEvents: function(targetNode /*, ... */) {
var datatype = this.options.datatype,
optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
@@ -70,6 +199,24 @@ var UIElement = L.Class.extend({
}, this));
},
+ /**
+ * Setup listeners for native DOM events that may change the widget value.
+ *
+ * Sets up event handlers on the given target DOM node for the given event
+ * names which may cause the input value to change completely, such as
+ * `change` events in a select menu. In contrast to update events, such
+ * change events will not trigger input value validation but they may cause
+ * field dependencies to get re-evaluated and will mark the input widget
+ * as dirty.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ * @param {Node} targetNode
+ * Specifies the DOM node on which the event listeners should be registered.
+ *
+ * @param {...string} events
+ * The DOM events for which event handlers should be registered.
+ */
setChangeEvents: function(targetNode /*, ... */) {
var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
@@ -77,10 +224,71 @@ var UIElement = L.Class.extend({
targetNode.addEventListener(arguments[i], tag_changed);
this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
- }
+ },
+
+ /**
+ * Render the widget, setup event listeners and return resulting markup.
+ *
+ * @instance
+ * @memberof LuCI.ui.AbstractElement
+ *
+ * @returns {Node}
+ * Returns a DOM Node or DocumentFragment containing the rendered
+ * widget markup.
+ */
+ render: function() {}
});
-var UITextfield = UIElement.extend({
+/**
+ * Instantiate a text input widget.
+ *
+ * @constructor Textfield
+ * @memberof LuCI.ui
+ * @augments LuCI.ui.AbstractElement
+ *
+ * @classdesc
+ *
+ * The `Textfield` class implements a standard single line text input field.
+ *
+ * UI widget instances are usually not supposed to be created by view code
+ * directly, instead they're implicitely created by `LuCI.form` when
+ * instantiating CBI forms.
+ *
+ * This class is automatically instantiated as part of `LuCI.ui`. To use it
+ * in views, use `'require ui'` and refer to `ui.Textfield`. To import it in
+ * external JavaScript, use `L.require("ui").then(...)` and access the
+ * `Textfield` property of the class instance value.
+ *
+ * @param {string} [value=null]
+ * The initial input value.
+ *
+ * @param {LuCI.ui.Textfield.InitOptions} [options]
+ * Object describing the widget specific options to initialize the input.
+ */
+var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
+ /**
+ * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
+ * the following properties are recognized:
+ *
+ * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
+ * @memberof LuCI.ui.Textfield
+ *
+ * @property {boolean} [password=false]
+ * Specifies whether the input should be rendered as concealed password field.
+ *
+ * @property {boolean} [readonly=false]
+ * Specifies whether the input widget should be rendered readonly.
+ *
+ * @property {number} [maxlength]
+ * Specifies the HTML `maxlength` attribute to set on the corresponding
+ * `` element. Note that this a legacy property that exists for
+ * compatibility reasons. It is usually better to `maxlength(N)` validation
+ * expression.
+ *
+ * @property {string} [placeholder]
+ * Specifies the HTML `placeholder` attribute which is displayed when the
+ * corresponding `` element is empty.
+ */
__init__: function(value, options) {
this.value = value;
this.options = Object.assign({
@@ -89,6 +297,7 @@ var UITextfield = UIElement.extend({
}, options);
},
+ /** @override */
render: function() {
var frameEl = E('div', { 'id': this.options.id });
@@ -129,6 +338,7 @@ var UITextfield = UIElement.extend({
return this.bind(frameEl);
},
+ /** @private */
bind: function(frameEl) {
var inputEl = frameEl.childNodes[+!!this.options.password];
@@ -142,18 +352,75 @@ var UITextfield = UIElement.extend({
return frameEl;
},
+ /** @override */
getValue: function() {
var inputEl = this.node.childNodes[+!!this.options.password];
return inputEl.value;
},
+ /** @override */
setValue: function(value) {
var inputEl = this.node.childNodes[+!!this.options.password];
inputEl.value = value;
}
});
-var UITextarea = UIElement.extend({
+/**
+ * Instantiate a textarea widget.
+ *
+ * @constructor Textarea
+ * @memberof LuCI.ui
+ * @augments LuCI.ui.AbstractElement
+ *
+ * @classdesc
+ *
+ * The `Textarea` class implements a multiline text area input field.
+ *
+ * UI widget instances are usually not supposed to be created by view code
+ * directly, instead they're implicitely created by `LuCI.form` when
+ * instantiating CBI forms.
+ *
+ * This class is automatically instantiated as part of `LuCI.ui`. To use it
+ * in views, use `'require ui'` and refer to `ui.Textarea`. To import it in
+ * external JavaScript, use `L.require("ui").then(...)` and access the
+ * `Textarea` property of the class instance value.
+ *
+ * @param {string} [value=null]
+ * The initial input value.
+ *
+ * @param {LuCI.ui.Textarea.InitOptions} [options]
+ * Object describing the widget specific options to initialize the input.
+ */
+var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
+ /**
+ * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
+ * the following properties are recognized:
+ *
+ * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
+ * @memberof LuCI.ui.Textarea
+ *
+ * @property {boolean} [readonly=false]
+ * Specifies whether the input widget should be rendered readonly.
+ *
+ * @property {string} [placeholder]
+ * Specifies the HTML `placeholder` attribute which is displayed when the
+ * corresponding `