summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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
});