From 1bb27177296cbfc4ad4a07dc59edf31fb8f92c7a Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 31 Mar 2020 21:27:42 +0200 Subject: luci-base: ui.js: add documentation Signed-off-by: Jo-Philipp Wich --- .../luci-base/htdocs/luci-static/resources/ui.js | 1315 +++++++++++++++++++- 1 file changed, 1292 insertions(+), 23 deletions(-) (limited to 'modules/luci-base') 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 `