'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 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.
*/
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 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: 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);
},
/**
* Returns the current validation error
*
* @instance
* @memberof LuCI.ui.AbstractElement
* @returns {string}
* The validation error at this time
*/
getValidationError: function() {
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: 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);
},
/**
* 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: 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.validationError = '';
}, this));
this.node.addEventListener('validation-failure', L.bind(function(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: 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, 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: 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 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.
*/
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,
'autocomplete': this.options.password ? 'new-password' : null,
'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 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.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 `