'use strict'; 'require validation'; 'require baseclass'; 'require request'; 'require session'; 'require poll'; 'require dom'; 'require rpc'; 'require uci'; 'require fs'; var modalDiv = null, tooltipDiv = null, indicatorDiv = null, tooltipTimeout = null; /** * @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 = baseclass.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. * * @property {boolean} [disabled=false] * Specifies whether the widget should be rendered in disabled state * (`true`) or not (`false`). Disabled widgets cannot be interacted with * and are displayed in a slightly faded style. */ /** * 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 (dom.matches(this.node, 'select') || dom.matches(this.node, 'input')) return this.node.value; 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 (dom.matches(this.node, 'select') || dom.matches(this.node, 'input')) this.node.value = value; }, /** * Set the current placeholder value of the input widget. * * @instance * @memberof LuCI.ui.AbstractElement * @param {string|string[]|null} value * The placeholder to set for the input element. Only applicable to text * inputs, not to radio buttons, selects or similar. */ setPlaceholder: function(value) { var node = this.node ? this.node.querySelector('input,textarea') : null; if (node) { switch (node.getAttribute('type') || 'text') { case 'password': case 'search': case 'tel': case 'text': case 'url': if (value != null && value != '') node.setAttribute('placeholder', value); else node.removeAttribute('placeholder'); } } }, /** * Check whether the input value was altered by the user. * * @instance * @memberof LuCI.ui.AbstractElement * @returns {boolean} * Returns `true` if the input value has been altered by the user or * `false` if it is unchaged. Note that if the user modifies the initial * value and changes it back to the original state, it is still reported * as changed. */ isChanged: function() { return (this.node ? this.node.getAttribute('data-changed') : null) == 'true'; }, /** * 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; var wasValid = this.isValid(); this.vfunc(); 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 })); }, this); for (var i = 0; i < events.length; i++) 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, validate = this.options.validate, events = this.varargs(arguments, 1); this.registerEvents(targetNode, 'widget-update', events); if (!datatype && !validate) return; this.vfunc = UI.prototype.addValidator.apply(UI.prototype, [ targetNode, datatype || 'string', optional, validate ].concat(events)); this.node.addEventListener('validation-success', L.bind(function(ev) { this.validState = true; }, this)); this.node.addEventListener('validation-failure', L.bind(function(ev) { this.validState = false; }, 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); for (var i = 1; i < arguments.length; i++) 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() {} }); /** * 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({ optional: true, password: false }, options); }, /** @override */ render: function() { var frameEl = E('div', { 'id': this.options.id }); var inputEl = E('input', { 'id': this.options.id ? 'widget.' + this.options.id : null, 'name': this.options.name, 'type': 'text', 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text', 'readonly': this.options.readonly ? '' : null, 'disabled': this.options.disabled ? '' : null, 'maxlength': this.options.maxlength, 'placeholder': this.options.placeholder, 'value': this.value, }); if (this.options.password) { frameEl.appendChild(E('div', { 'class': 'control-group' }, [ inputEl, E('button', { 'class': 'cbi-button cbi-button-neutral', 'title': _('Reveal/hide password'), 'aria-label': _('Reveal/hide password'), 'click': function(ev) { var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'; ev.preventDefault(); } }, '∗') ])); window.requestAnimationFrame(function() { inputEl.type = 'password' }); } else { frameEl.appendChild(inputEl); } return this.bind(frameEl); }, /** @private */ bind: function(frameEl) { var inputEl = frameEl.querySelector('input'); this.node = frameEl; this.setUpdateEvents(inputEl, 'keyup', 'blur'); this.setChangeEvents(inputEl, 'change'); dom.bindClassInstance(frameEl, this); return frameEl; }, /** @override */ getValue: function() { var inputEl = this.node.querySelector('input'); return inputEl.value; }, /** @override */ setValue: function(value) { var inputEl = this.node.querySelector('input'); inputEl.value = value; } }); /** * 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 `