'use strict';
'require validation';
'require baseclass';
'require request';
'require session';
'require poll';
'require dom';
'require rpc';
'require uci';
'require fs';
let modalDiv = null;
let tooltipDiv = null;
let indicatorDiv = null;
let 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 implicitly 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.
*/
const 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() {
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(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(value) {
const 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 unchanged. Note that if the user modifies the initial
* value and changes it back to the original state, it is still reported
* as changed.
*/
isChanged() {
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() {
return (this.validState !== false);
},
/**
* Returns the current validation error
*
* @instance
* @memberof LuCI.ui.AbstractElement
* @returns {string}
* The validation error at this time
*/
getValidationError() {
return this.validationError ?? '';
},
/**
* 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() {
if (typeof(this.vfunc) != 'function')
return false;
const 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(targetNode, synevent, events) {
const dispatchFn = L.bind((ev) => {
this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
}, this);
for (let i = 0; i < events.length; i++)
targetNode.addEventListener(events[i], dispatchFn);
},
/**
* Set up 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(targetNode, ...events) {
const datatype = this.options.datatype;
const optional = this.options.hasOwnProperty('optional') ? this.options.optional : true;
const validate = this.options.validate;
this.registerEvents(targetNode, 'widget-update', events);
if (!datatype && !validate)
return;
this.vfunc = UI.prototype.addValidator(...[
targetNode, datatype ?? 'string',
optional, validate
].concat(events));
this.node.addEventListener('validation-success', L.bind((ev) => {
this.validState = true;
this.validationError = '';
}, this));
this.node.addEventListener('validation-failure', L.bind((ev) => {
this.validState = false;
this.validationError = ev.detail.message;
}, this));
},
/**
* Set up 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(targetNode, ...events) {
const tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
for (let i = 0; i < events.length; i++)
targetNode.addEventListener(events[i], tag_changed);
this.registerEvents(targetNode, 'widget-change', events);
},
/**
* Render the widget, set up 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() {}
});
/**
* 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 implicitly 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.
*/
const UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
*
* @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__(value, options) {
this.value = value;
this.options = Object.assign({
optional: true,
password: false
}, options);
},
/** @override */
render() {
const frameEl = E('div', { 'id': this.options.id });
const inputEl = E('input', {
'id': this.options.id ? `widget.${this.options.id}` : null,
'name': this.options.name,
'type': 'text',
'class': `password-input ${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) {
// DOM manipulation (e.g. by password managers) may have inserted other
// elements between the reveal button and the input. This searches for
// the first inside the parent of the