summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2019-04-01 16:00:10 +0200
committerJo-Philipp Wich <jo@mein.io>2019-07-07 15:36:23 +0200
commit808b9f36eb60cf4717cbf28b163d7ad2faa7f157 (patch)
tree466548e3bd93727a67b9016fc399ba31b2e0da66 /modules/luci-base
parent2beb9fa16fffc0cceea1d90c191309dfbcc307cc (diff)
luci-base: cbi.js, ui.js: add custom validation callbacks, new ui widgets
Implement further widget primitives like text inputs or checkboxes and support custom validation callback functions when instantiating CBI validators. Also add support initializing ui.js widgets from the "data-ui-widget" HTML attribute. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-base')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js52
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js368
2 files changed, 395 insertions, 25 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index 745f01244b..1c5e4133ad 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -260,11 +260,11 @@ var CBIValidatorPrototype = {
validate: function() {
/* element is detached */
- if (!findParent(this.field, 'body'))
+ if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
return true;
this.field.classList.remove('cbi-input-invalid');
- this.value = matchesElem(this.field, 'select') ? this.field.options[this.field.selectedIndex].value : this.field.value;
+ this.value = (this.field.value != null) ? this.field.value : '';
this.error = null;
var valid;
@@ -274,18 +274,28 @@ var CBIValidatorPrototype = {
else
valid = this.vstack[0].apply(this, this.vstack[1]);
- if (!valid) {
+ if (valid !== true) {
this.field.setAttribute('data-tooltip', _('Expecting %s').format(this.error));
this.field.setAttribute('data-tooltip-style', 'error');
this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
+ return false;
}
- else {
- this.field.removeAttribute('data-tooltip');
- this.field.removeAttribute('data-tooltip-style');
- this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
+
+ if (typeof(this.vfunc) == 'function')
+ valid = this.vfunc(this.value);
+
+ if (valid !== true) {
+ this.assert(false, valid);
+ this.field.setAttribute('data-tooltip', valid);
+ this.field.setAttribute('data-tooltip-style', 'error');
+ this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
+ return false;
}
- return valid;
+ this.field.removeAttribute('data-tooltip');
+ this.field.removeAttribute('data-tooltip-style');
+ this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
+ return true;
},
types: {
@@ -600,10 +610,14 @@ var CBIValidatorPrototype = {
if (sibling === option)
return;
- var input = sibling.querySelector('[data-type]'),
- values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
+ var values = L.dom.callClassMethod(sibling.querySelector('[data-idref]'), 'getValue');
- if (values !== null && values.indexOf(ctx.value) !== -1)
+ if (!Array.isArray(values) && sibling.querySelector('[data-is-list]'))
+ values = String(values || '').match(/[^ \t]+/g) || [];
+ else if (!Array.isArray(values))
+ values = (values != null) ? [ values ] : [];
+
+ if (values.indexOf(ctx.value) != -1)
unique = false;
});
@@ -619,14 +633,19 @@ var CBIValidatorPrototype = {
hexstring: function() {
return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
_('hexadecimal encoded value'));
+ },
+
+ string: function() {
+ return true;
}
}
};
-function CBIValidator(field, type, optional)
+function CBIValidator(field, type, optional, vfunc)
{
this.field = field;
this.optional = optional;
+ this.vfunc = vfunc;
this.vstack = this.compile(type);
}
@@ -850,6 +869,15 @@ function cbi_init() {
i.addEventListener('mouseout', handler);
});
+ document.querySelectorAll('[data-ui-widget]').forEach(function(node) {
+ var args = JSON.parse(node.getAttribute('data-ui-widget') || '[]'),
+ widget = new (Function.prototype.bind.apply(L.ui[args[0]], args)),
+ markup = widget.render();
+
+ markup.addEventListener('widget-change', cbi_d_update);
+ node.parentNode.replaceChild(markup, node);
+ });
+
cbi_d_update();
}
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index c80acdfd23..7d04d7807c 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -19,7 +19,18 @@ var UIElement = L.Class.extend({
},
isValid: function() {
- return true;
+ return (this.validState !== false);
+ },
+
+ triggerValidation: function() {
+ if (typeof(this.vfunc) != 'function')
+ return false;
+
+ var wasValid = this.isValid();
+
+ this.vfunc();
+
+ return (wasValid != this.isValid());
},
registerEvents: function(targetNode, synevent, events) {
@@ -32,7 +43,28 @@ var UIElement = L.Class.extend({
},
setUpdateEvents: function(targetNode /*, ... */) {
- this.registerEvents(targetNode, 'widget-update', this.varargs(arguments, 1));
+ 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 = L.ui.addValidator.apply(L.ui, [
+ 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));
},
setChangeEvents: function(targetNode /*, ... */) {
@@ -40,13 +72,273 @@ var UIElement = L.Class.extend({
}
});
+var UITextfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ optional: true,
+ password: false
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id });
+
+ if (this.options.password) {
+ frameEl.classList.add('nowrap');
+ frameEl.appendChild(E('input', {
+ 'type': 'password',
+ 'style': 'position:absolute; left:-100000px',
+ 'aria-hidden': true,
+ 'tabindex': -1,
+ 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
+ }));
+ }
+
+ frameEl.appendChild(E('input', {
+ 'name': this.options.name,
+ 'type': this.options.password ? 'password' : 'text',
+ 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
+ 'placeholder': this.options.placeholder,
+ 'value': this.value,
+ }));
+
+ if (this.options.password)
+ frameEl.appendChild(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();
+ }
+ }, '∗'));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ var inputEl = frameEl.childNodes[+!!this.options.password];
+
+ this.node = frameEl;
+
+ this.setUpdateEvents(inputEl, 'keyup', 'blur');
+ this.setChangeEvents(inputEl, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ return inputEl.value;
+ },
+
+ setValue: function(value) {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ inputEl.value = value;
+ }
+});
+
+var UICheckbox = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ value_enabled: '1',
+ value_disabled: '0'
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', {
+ 'id': this.options.id,
+ 'class': 'cbi-checkbox'
+ });
+
+ if (this.options.hiddenname)
+ frameEl.appendChild(E('input', {
+ 'type': 'hidden',
+ 'name': this.options.hiddenname,
+ 'value': 1
+ }));
+
+ frameEl.appendChild(E('input', {
+ 'name': this.options.name,
+ 'type': 'checkbox',
+ 'value': this.options.value_enabled,
+ 'checked': (this.value == this.options.value_enabled) ? '' : null
+ }));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
+ this.setChangeEvents(frameEl.lastElementChild, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ isChecked: function() {
+ return this.node.lastElementChild.checked;
+ },
+
+ getValue: function() {
+ return this.isChecked()
+ ? this.options.value_enabled
+ : this.options.value_disabled;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.checked = (value == this.options.value_enabled);
+ }
+});
+
+var UISelect = UIElement.extend({
+ __init__: function(value, choices, options) {
+ if (typeof(choices) != 'object')
+ choices = {};
+
+ if (!Array.isArray(value))
+ value = (value != null && value != '') ? [ value ] : [];
+
+ if (!options.multi && value.length > 1)
+ value.length = 1;
+
+ this.values = value;
+ this.choices = choices;
+ this.options = Object.assign({
+ multi: false,
+ widget: 'select',
+ orientation: 'horizontal'
+ }, options);
+ },
+
+ render: function() {
+ var frameEl,
+ keys = Object.keys(this.choices);
+
+ if (this.options.sort === true)
+ keys.sort();
+ else if (Array.isArray(this.options.sort))
+ keys = this.options.sort;
+
+ if (this.options.widget == 'select') {
+ frameEl = E('select', {
+ 'id': this.options.id,
+ 'name': this.options.name,
+ 'size': this.options.size,
+ 'class': 'cbi-input-select',
+ 'multiple': this.options.multi ? '' : null
+ });
+
+ if (this.options.optional)
+ frameEl.appendChild(E('option', {
+ 'value': '',
+ 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
+ }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
+
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i] == null || keys[i] == '')
+ continue;
+
+ frameEl.appendChild(E('option', {
+ 'value': keys[i],
+ 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }, this.choices[keys[i]] || keys[i]));
+ }
+ }
+ else {
+ frameEl = E('div', {
+ 'id': this.options.id
+ });
+
+ var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
+
+ for (var i = 0; i < keys.length; i++) {
+ frameEl.appendChild(E('label', {}, [
+ E('input', {
+ 'name': this.options.id || this.options.name,
+ 'type': this.options.multi ? 'checkbox' : 'radio',
+ 'class': this.options.multi ? 'cbi-input-checkbox' : 'cbi-input-radio',
+ 'value': keys[i],
+ 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }),
+ this.choices[keys[i]] || keys[i]
+ ]));
+
+ if (i + 1 == this.options.size)
+ frameEl.appendChild(brEl);
+ }
+ }
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ if (this.options.widget == 'select') {
+ this.setUpdateEvents(frameEl, 'change', 'click', 'blur');
+ this.setChangeEvents(frameEl, 'change');
+ }
+ else {
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++) {
+ this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
+ this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
+ }
+ }
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ if (this.options.widget == 'select')
+ return this.node.value;
+
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++)
+ if (radioEls[i].checked)
+ return radioEls[i].value;
+
+ return null;
+ },
+
+ setValue: function(value) {
+ if (this.options.widget == 'select') {
+ if (value == null)
+ value = '';
+
+ for (var i = 0; i < this.node.options.length; i++)
+ this.node.options[i].selected = (this.node.options[i].value == value);
+
+ return;
+ }
+
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++)
+ radioEls[i].checked = (radioEls[i].value == value);
+ }
+});
+
var UIDropdown = UIElement.extend({
__init__: function(value, choices, options) {
if (typeof(choices) != 'object')
choices = {};
if (!Array.isArray(value))
- this.values = (value != null) ? [ value ] : [];
+ this.values = (value != null && value != '') ? [ value ] : [];
else
this.values = value;
@@ -95,15 +387,22 @@ var UIDropdown = UIElement.extend({
var createEl = E('input', {
'type': 'text',
'class': 'create-item-input',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
'placeholder': this.options.custom_placeholder || this.options.placeholder
});
if (this.options.datatype)
- L.ui.addValidator(createEl, this.options.datatype, true, 'blur', 'keyup');
+ L.ui.addValidator(createEl, this.options.datatype,
+ true, null, 'blur', 'keyup');
sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
}
+ if (this.options.create_markup)
+ sb.appendChild(E('script', { type: 'item-template' },
+ this.options.create_markup));
+
return this.bind(sb);
},
@@ -751,7 +1050,7 @@ var UIDropdown = UIElement.extend({
setValue: function(values) {
if (this.options.multi) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
var v = {};
@@ -791,9 +1090,9 @@ var UICombobox = UIDropdown.extend({
this.super('__init__', [ value, choices, Object.assign({
select_placeholder: _('-- Please choose --'),
custom_placeholder: _('-- custom --'),
- dropdown_items: 5
+ dropdown_items: 5,
+ sort: true
}, options, {
- sort: true,
multi: false,
create: true,
optional: true
@@ -804,7 +1103,7 @@ var UICombobox = UIDropdown.extend({
var UIDynamicList = UIElement.extend({
__init__: function(values, choices, options) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
if (typeof(choices) != 'object')
choices = null;
@@ -837,7 +1136,9 @@ var UIDynamicList = UIElement.extend({
dl.lastElementChild.appendChild(inputEl);
dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
- L.ui.addValidator(inputEl, this.options.datatype, true, 'blue', 'keyup');
+ if (this.options.datatype)
+ L.ui.addValidator(inputEl, this.options.datatype,
+ true, null, 'blur', 'keyup');
}
for (var i = 0; i < this.values.length; i++)
@@ -1012,7 +1313,7 @@ var UIDynamicList = UIElement.extend({
setValue: function(values) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
var items = this.node.querySelectorAll('.item');
@@ -1026,6 +1327,41 @@ var UIDynamicList = UIElement.extend({
}
});
+var UIHiddenfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+
+ }, options);
+ },
+
+ render: function() {
+ var hiddenEl = E('input', {
+ 'id': this.options.id,
+ 'type': 'hidden',
+ 'value': this.value
+ });
+
+ return this.bind(hiddenEl);
+ },
+
+ bind: function(hiddenEl) {
+ this.node = hiddenEl;
+
+ L.dom.bindClassInstance(hiddenEl, this);
+
+ return hiddenEl;
+ },
+
+ getValue: function() {
+ return this.node.value;
+ },
+
+ setValue: function(value) {
+ this.node.value = value;
+ }
+});
+
return L.Class.extend({
__init__: function() {
@@ -1658,7 +1994,7 @@ return L.Class.extend({
}
}),
- addValidator: function(field, type, optional /*, ... */) {
+ addValidator: function(field, type, optional, vfunc /*, ... */) {
if (type == null)
return;
@@ -1667,19 +2003,25 @@ return L.Class.extend({
events.push('blur', 'keyup');
try {
- var cbiValidator = new CBIValidator(field, type, optional),
+ var cbiValidator = new CBIValidator(field, type, optional, vfunc),
validatorFn = cbiValidator.validate.bind(cbiValidator);
for (var i = 0; i < events.length; i++)
field.addEventListener(events[i], validatorFn);
validatorFn();
+
+ return validatorFn;
}
catch (e) { }
},
/* Widgets */
+ Textfield: UITextfield,
+ Checkbox: UICheckbox,
+ Select: UISelect,
Dropdown: UIDropdown,
DynamicList: UIDynamicList,
- Combobox: UICombobox
+ Combobox: UICombobox,
+ Hiddenfield: UIHiddenfield
});