summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs')
-rwxr-xr-xmodules/luci-base/htdocs/cgi-bin/luci2
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js18
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/firewall.js25
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js288
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js38
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/network.js389
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js30
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/protocol/static.js40
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/rpc.js3
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/prng.js18
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js3
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/uci.js84
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js136
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/validation.js35
14 files changed, 818 insertions, 291 deletions
diff --git a/modules/luci-base/htdocs/cgi-bin/luci b/modules/luci-base/htdocs/cgi-bin/luci
index 529d1d0bc5..c5c9847346 100755
--- a/modules/luci-base/htdocs/cgi-bin/luci
+++ b/modules/luci-base/htdocs/cgi-bin/luci
@@ -2,4 +2,4 @@
require "luci.cacheloader"
require "luci.sgi.cgi"
luci.dispatcher.indexcache = "/tmp/luci-indexcache"
-luci.sgi.cgi.run() \ No newline at end of file
+luci.sgi.cgi.run()
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index 92c41515fb..b66fe684a5 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -744,22 +744,22 @@ function cbi_update_table(table, data, placeholder) {
if (!isElem(target))
return;
- target.querySelectorAll('.tr.table-titles, .cbi-section-table-titles').forEach(function(thead) {
+ target.querySelectorAll('tr.table-titles, .tr.table-titles, .cbi-section-table-titles').forEach(function(thead) {
var titles = [];
- thead.querySelectorAll('.th').forEach(function(th) {
+ thead.querySelectorAll('th, .th').forEach(function(th) {
titles.push(th);
});
if (Array.isArray(data)) {
- var n = 0, rows = target.querySelectorAll('.tr');
+ var n = 0, rows = target.querySelectorAll('tr, .tr');
data.forEach(function(row) {
- var trow = E('div', { 'class': 'tr' });
+ var trow = E('tr', { 'class': 'tr' });
for (var i = 0; i < titles.length; i++) {
var text = (titles[i].innerText || '').trim();
- var td = trow.appendChild(E('div', {
+ var td = trow.appendChild(E('td', {
'class': titles[i].className,
'data-title': (text !== '') ? text : null
}, row[i] || ''));
@@ -780,8 +780,8 @@ function cbi_update_table(table, data, placeholder) {
target.removeChild(rows[n]);
if (placeholder && target.firstElementChild === target.lastElementChild) {
- var trow = target.appendChild(E('div', { 'class': 'tr placeholder' }));
- var td = trow.appendChild(E('div', { 'class': titles[0].className }, placeholder));
+ var trow = target.appendChild(E('tr', { 'class': 'tr placeholder' }));
+ var td = trow.appendChild(E('td', { 'class': titles[0].className }, placeholder));
td.classList.remove('th');
td.classList.add('td');
@@ -790,10 +790,10 @@ function cbi_update_table(table, data, placeholder) {
else {
thead.parentNode.style.display = 'none';
- thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) {
+ thead.parentNode.querySelectorAll('tr, .tr, .cbi-section-table-row').forEach(function(trow) {
if (trow !== thead) {
var n = 0;
- trow.querySelectorAll('.th, .td').forEach(function(td) {
+ trow.querySelectorAll('th, td, .th, .td').forEach(function(td) {
if (n < titles.length) {
var text = (titles[n++].innerText || '').trim();
if (text !== '')
diff --git a/modules/luci-base/htdocs/luci-static/resources/firewall.js b/modules/luci-base/htdocs/luci-static/resources/firewall.js
index b1c7de4358..4fa4954ba6 100644
--- a/modules/luci-base/htdocs/luci-static/resources/firewall.js
+++ b/modules/luci-base/htdocs/luci-static/resources/firewall.js
@@ -57,21 +57,7 @@ function getColorForName(forName) {
else if (forName == 'wan')
return '#f09090';
- random.seed(parseInt(sfh(forName), 16));
-
- var r = random.get(128),
- g = random.get(128),
- min = 0,
- max = 128;
-
- if ((r + g) < 128)
- min = 128 - r - g;
- else
- max = 255 - r - g;
-
- var b = min + Math.floor(random.get() * (max - min));
-
- return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b);
+ return random.derive_color(forName);
}
@@ -106,7 +92,6 @@ Firewall = L.Class.extend({
z = uci.add('firewall', 'zone');
uci.set('firewall', z, 'name', name);
- uci.set('firewall', z, 'network', ' ');
uci.set('firewall', z, 'input', d.getInput() || 'DROP');
uci.set('firewall', z, 'output', d.getOutput() || 'DROP');
uci.set('firewall', z, 'forward', d.getForward() || 'DROP');
@@ -347,17 +332,17 @@ Zone = AbstractFirewallItem.extend({
return false;
newNetworks.push(network);
- this.set('network', newNetworks.join(' '));
+ this.set('network', newNetworks);
return true;
},
deleteNetwork: function(network) {
var oldNetworks = this.getNetworks(),
- newNetworks = oldNetworks.filter(function(net) { return net != network });
+ newNetworks = oldNetworks.filter(function(net) { return net != network });
if (newNetworks.length > 0)
- this.set('network', newNetworks.join(' '));
+ this.set('network', newNetworks);
else
this.set('network', null);
@@ -369,7 +354,7 @@ Zone = AbstractFirewallItem.extend({
},
clearNetworks: function() {
- this.set('network', ' ');
+ this.set('network', null);
},
getDevices: function() {
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
index a50d457e21..52506d30e8 100644
--- a/modules/luci-base/htdocs/luci-static/resources/form.js
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -603,7 +603,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
E('p', {}, [ _('An error occurred while saving the form:') ]),
E('p', {}, [ E('em', { 'style': 'white-space:pre' }, [ e.message ]) ]),
E('div', { 'class': 'right' }, [
- E('button', { 'click': ui.hideModal }, [ _('Dismiss') ])
+ E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Dismiss') ])
])
]);
}
@@ -1054,6 +1054,138 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
return obj;
},
+ /**
+ * Query underlying option configuration values.
+ *
+ * This function is sensitive to the amount of arguments passed to it;
+ * if only one argument is specified, the configuration values of all
+ * options within this section are returned as dictionary.
+ *
+ * If both the section ID and an option name are supplied, this function
+ * returns the configuration value of the specified option only.
+ *
+ * @param {string} section_id
+ * The configuration section ID
+ *
+ * @param {string} [option]
+ * The name of the option to query
+ *
+ * @returns {null|string|string[]|Object<string, null|string|string[]>}
+ * Returns either a dictionary of option names and their corresponding
+ * configuration values or just a single configuration value, depending
+ * on the amount of passed arguments.
+ */
+ cfgvalue: function(section_id, option) {
+ var rv = (arguments.length == 1) ? {} : null;
+
+ for (var i = 0, o; (o = this.children[i]) != null; i++)
+ if (rv)
+ rv[o.option] = o.cfgvalue(section_id);
+ else if (o.option == option)
+ return o.cfgvalue(section_id);
+
+ return rv;
+ },
+
+ /**
+ * Query underlying option widget input values.
+ *
+ * This function is sensitive to the amount of arguments passed to it;
+ * if only one argument is specified, the widget input values of all
+ * options within this section are returned as dictionary.
+ *
+ * If both the section ID and an option name are supplied, this function
+ * returns the widget input value of the specified option only.
+ *
+ * @param {string} section_id
+ * The configuration section ID
+ *
+ * @param {string} [option]
+ * The name of the option to query
+ *
+ * @returns {null|string|string[]|Object<string, null|string|string[]>}
+ * Returns either a dictionary of option names and their corresponding
+ * widget input values or just a single widget input value, depending
+ * on the amount of passed arguments.
+ */
+ formvalue: function(section_id, option) {
+ var rv = (arguments.length == 1) ? {} : null;
+
+ for (var i = 0, o; (o = this.children[i]) != null; i++) {
+ var func = this.map.root ? this.children[i].formvalue : this.children[i].cfgvalue;
+
+ if (rv)
+ rv[o.option] = func.call(o, section_id);
+ else if (o.option == option)
+ return func.call(o, section_id);
+ }
+
+ return rv;
+ },
+
+ /**
+ * Obtain underlying option LuCI.ui widget instances.
+ *
+ * This function is sensitive to the amount of arguments passed to it;
+ * if only one argument is specified, the LuCI.ui widget instances of all
+ * options within this section are returned as dictionary.
+ *
+ * If both the section ID and an option name are supplied, this function
+ * returns the LuCI.ui widget instance value of the specified option only.
+ *
+ * @param {string} section_id
+ * The configuration section ID
+ *
+ * @param {string} [option]
+ * The name of the option to query
+ *
+ * @returns {null|LuCI.ui.AbstractElement|Object<string, null|LuCI.ui.AbstractElement>}
+ * Returns either a dictionary of option names and their corresponding
+ * widget input values or just a single widget input value, depending
+ * on the amount of passed arguments.
+ */
+ getUIElement: function(section_id, option) {
+ var rv = (arguments.length == 1) ? {} : null;
+
+ for (var i = 0, o; (o = this.children[i]) != null; i++)
+ if (rv)
+ rv[o.option] = o.getUIElement(section_id);
+ else if (o.option == option)
+ return o.getUIElement(section_id);
+
+ return rv;
+ },
+
+ /**
+ * Obtain underlying option objects.
+ *
+ * This function is sensitive to the amount of arguments passed to it;
+ * if no option name is specified, all options within this section are
+ * returned as dictionary.
+ *
+ * If an option name is supplied, this function returns the matching
+ * LuCI.form.AbstractValue instance only.
+ *
+ * @param {string} [option]
+ * The name of the option object to obtain
+ *
+ * @returns {null|LuCI.form.AbstractValue|Object<string, LuCI.form.AbstractValue>}
+ * Returns either a dictionary of option names and their corresponding
+ * option instance objects or just a single object instance value,
+ * depending on the amount of passed arguments.
+ */
+ getOption: function(option) {
+ var rv = (arguments.length == 0) ? {} : null;
+
+ for (var i = 0, o; (o = this.children[i]) != null; i++)
+ if (rv)
+ rv[o.option] = o;
+ else if (o.option == option)
+ return o;
+
+ return rv;
+ },
+
/** @private */
renderUCISection: function(section_id) {
var renderTasks = [];
@@ -1132,6 +1264,9 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
var isEqual = function(x, y) {
+ if (typeof(y) == 'object' && y instanceof RegExp)
+ return (x == null) ? false : y.test(x);
+
if (x != null && y != null && typeof(x) != typeof(y))
return false;
@@ -1372,6 +1507,21 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
*/
/**
+ * Register a custom value change handler.
+ *
+ * If this property is set to a function value, the function is invoked
+ * whenever the value of the underlying UI input element is changing.
+ *
+ * The invoked handler function will receive the DOM click element as
+ * first and the underlying configuration section ID as well as the input
+ * value as second and third argument respectively.
+ *
+ * @name LuCI.form.AbstractValue.prototype#onchange
+ * @type function
+ * @default null
+ */
+
+ /**
* Add a dependency contraint to the option.
*
* Dependency constraints allow making the presence of option elements
@@ -1428,6 +1578,10 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
* Equivalent to the previous example.
* </li>
* <li>
+ * <code>opt.depends({ foo: /test/ })</code><br>
+ * Require the value of `foo` to match the regular expression `/test/`.
+ * </li>
+ * <li>
* <code>opt.depends({ foo: "test", bar: "qrx" })</code><br>
* Require the value of `foo` to be `test` and the value of `bar` to be
* `qrx`.
@@ -1449,11 +1603,11 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
* </li>
* </ul>
*
- * @param {string|Object<string, string|boolean>} optionname_or_depends
+ * @param {string|Object<string, string|RegExp>} optionname_or_depends
* The name of the option to depend on or an object describing multiple
* dependencies which must be satified (a logical "and" expression).
*
- * @param {string} optionvalue
+ * @param {string} optionvalue|RegExp
* When invoked with a plain option name as first argument, this parameter
* specifies the expected value. In case an object is passed as first
* argument, this parameter is ignored.
@@ -1708,6 +1862,9 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
if (cval == null)
cval = this.default;
+ if (Array.isArray(cval))
+ cval = cval.join(' ');
+
return (cval != null) ? '%h'.format(cval) : null;
},
@@ -1878,10 +2035,32 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
* The configuration section ID
*/
remove: function(section_id) {
- return this.map.data.unset(
- this.uciconfig || this.section.uciconfig || this.map.config,
- this.ucisection || section_id,
- this.ucioption || this.option);
+ var this_cfg = this.uciconfig || this.section.uciconfig || this.map.config,
+ this_sid = this.ucisection || section_id,
+ this_opt = this.ucioption || this.option;
+
+ for (var i = 0; i < this.section.children.length; i++) {
+ var sibling = this.section.children[i];
+
+ if (sibling === this || sibling.ucioption == null)
+ continue;
+
+ var sibling_cfg = sibling.uciconfig || sibling.section.uciconfig || sibling.map.config,
+ sibling_sid = sibling.ucisection || section_id,
+ sibling_opt = sibling.ucioption || sibling.option;
+
+ if (this_cfg != sibling_cfg || this_sid != sibling_sid || this_opt != sibling_opt)
+ continue;
+
+ if (!sibling.isActive(section_id))
+ continue;
+
+ /* found another active option aliasing the same uci option name,
+ * so we can't remove the value */
+ return;
+ }
+
+ this.map.data.unset(this_cfg, this_sid, this_opt);
}
});
@@ -2065,7 +2244,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
});
if (this.title != null && this.title != '')
- sectionEl.appendChild(E('legend', {}, this.title));
+ sectionEl.appendChild(E('h3', {}, this.title));
if (this.description != null && this.description != '')
sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
@@ -2306,7 +2485,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
}),
- tableEl = E('div', {
+ tableEl = E('table', {
'class': 'table cbi-section-table'
});
@@ -2324,7 +2503,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (sectionname == null)
sectionname = cfgsections[i];
- var trEl = E('div', {
+ var trEl = E('tr', {
'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
'class': 'tr cbi-section-table-row',
'data-sid': cfgsections[i],
@@ -2352,8 +2531,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
if (nodes.length == 0)
- tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
- E('div', { 'class': 'td' },
+ tableEl.appendChild(E('tr', { 'class': 'tr cbi-section-table-row placeholder' },
+ E('td', { 'class': 'td' },
E('em', {}, _('This section contains no values yet')))));
sectionEl.appendChild(tableEl);
@@ -2383,7 +2562,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
if (has_titles) {
- var trEl = E('div', {
+ var trEl = E('tr', {
'class': 'tr cbi-section-table-titles ' + anon_class,
'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
});
@@ -2392,7 +2571,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (opt.modalonly)
continue;
- trEl.appendChild(E('div', {
+ trEl.appendChild(E('th', {
'class': 'th cbi-section-table-cell',
'data-widget': opt.__name__
}));
@@ -2412,7 +2591,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
if (this.sortable || this.extedit || this.addremove || has_more || has_action)
- trEl.appendChild(E('div', {
+ trEl.appendChild(E('th', {
'class': 'th cbi-section-table-cell cbi-section-actions'
}));
@@ -2420,7 +2599,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
if (has_descriptions && !this.nodescriptions) {
- var trEl = E('div', {
+ var trEl = E('tr', {
'class': 'tr cbi-section-table-descr ' + anon_class
});
@@ -2428,7 +2607,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (opt.modalonly)
continue;
- trEl.appendChild(E('div', {
+ trEl.appendChild(E('th', {
'class': 'th cbi-section-table-cell',
'data-widget': opt.__name__
}, opt.description));
@@ -2439,7 +2618,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
if (this.sortable || this.extedit || this.addremove || has_more || has_action)
- trEl.appendChild(E('div', {
+ trEl.appendChild(E('th', {
'class': 'th cbi-section-table-cell cbi-section-actions'
}));
@@ -2456,7 +2635,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (!this.sortable && !this.extedit && !this.addremove && !more_label)
return E([]);
- var tdEl = E('div', {
+ var tdEl = E('td', {
'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
}, E('div'));
@@ -2614,7 +2793,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
/** @private */
handleModalSave: function(modalMap, ev) {
- return modalMap.save()
+ return modalMap.save(null, true)
.then(L.bind(this.map.load, this.map))
.then(L.bind(this.map.reset, this.map))
.then(ui.hideModal)
@@ -2844,7 +3023,7 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
descr = this.stripTags(opt.description).trim(),
value = opt.textvalue(section_id);
- return E('div', {
+ return E('td', {
'class': 'td cbi-value-field',
'data-title': (title != '') ? title : null,
'data-description': (descr != '') ? descr : null,
@@ -2984,7 +3163,7 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
});
if (typeof(this.title) === 'string' && this.title !== '')
- sectionEl.appendChild(E('legend', {}, this.title));
+ sectionEl.appendChild(E('h3', {}, this.title));
if (typeof(this.description) === 'string' && this.description !== '')
sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
@@ -3114,6 +3293,20 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
},
/** @private */
+ handleValueChange: function(section_id, state, ev) {
+ if (typeof(this.onchange) != 'function')
+ return;
+
+ var value = this.formvalue(section_id);
+
+ if (isEqual(value, state.previousValue))
+ return;
+
+ state.previousValue = value;
+ this.onchange.call(this, ev, section_id, value);
+ },
+
+ /** @private */
renderFrame: function(section_id, in_table, option_index, nodes) {
var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
depend_list = this.transformDepList(section_id),
@@ -3121,7 +3314,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
if (in_table) {
var title = this.stripTags(this.title).trim();
- optionEl = E('div', {
+ optionEl = E('td', {
'class': 'td cbi-value-field',
'data-title': (title != '') ? title : null,
'data-description': this.stripTags(this.description).trim(),
@@ -3185,6 +3378,9 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
optionEl.addEventListener('widget-change',
L.bind(this.map.checkDepends, this.map));
+ optionEl.addEventListener('widget-change',
+ L.bind(this.handleValueChange, this, section_id, {}));
+
dom.bindClassInstance(optionEl, this);
return optionEl;
@@ -3431,13 +3627,47 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
* @default 0
*/
+ /**
+ * Set a tooltip for the flag option.
+ *
+ * If set to a string, it will be used as-is as a tooltip.
+ *
+ * If set to a function, the function will be invoked and the return
+ * value will be shown as a tooltip. If the return value of the function
+ * is `null` no tooltip will be set.
+ *
+ * @name LuCI.form.TypedSection.prototype#tooltip
+ * @type string|function
+ * @default null
+ */
+
+ /**
+ * Set a tooltip icon.
+ *
+ * If set, this icon will be shown for the default one.
+ * This could also be a png icon from the resources directory.
+ *
+ * @name LuCI.form.TypedSection.prototype#tooltipicon
+ * @type string
+ * @default 'ℹ️';
+ */
+
/** @private */
renderWidget: function(section_id, option_index, cfgvalue) {
+ var tooltip = null;
+
+ if (typeof(this.tooltip) == 'function')
+ tooltip = this.tooltip.apply(this, [section_id]);
+ else if (typeof(this.tooltip) == 'string')
+ tooltip = (arguments.length > 1) ? ''.format.apply(this.tooltip, this.varargs(arguments, 1)) : this.tooltip;
+
var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
id: this.cbid(section_id),
value_enabled: this.enabled,
value_disabled: this.disabled,
validate: L.bind(this.validate, this, section_id),
+ tooltip: tooltip,
+ tooltipicon: this.tooltipicon,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
@@ -3724,11 +3954,21 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
* @default null
*/
+ /**
+ * Render the UCI option value as hidden using the HTML display: none style property.
+ *
+ * By default, the value is displayed
+ *
+ * @name LuCI.form.DummyValue.prototype#hidden
+ * @type boolean
+ * @default null
+ */
+
/** @private */
renderWidget: function(section_id, option_index, cfgvalue) {
var value = (cfgvalue != null) ? cfgvalue : this.default,
hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
- outputEl = E('div');
+ outputEl = E('div', { 'style': this.hidden ? 'display:none' : null });
if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly))
outputEl.appendChild(E('a', { 'href': this.href }));
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 83c2807d77..23853e2cc8 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -672,12 +672,12 @@
* if it is an object, it will be converted to JSON, in all other
* cases it is converted to a string.
*
- * @property {Object<string, string>} [header]
- * Specifies HTTP headers to set for the request.
- *
- * @property {function} [progress]
- * An optional request callback function which receives ProgressEvent
- * instances as sole argument during the HTTP request transfer.
+ * @property {Object<string, string>} [header]
+ * Specifies HTTP headers to set for the request.
+ *
+ * @property {function} [progress]
+ * An optional request callback function which receives ProgressEvent
+ * instances as sole argument during the HTTP request transfer.
*/
/**
@@ -982,12 +982,13 @@
if (!Poll.active())
return;
+ var res_json = null;
try {
- callback(res, res.json(), res.duration);
- }
- catch (err) {
- callback(res, null, res.duration);
+ res_json = res.json();
}
+ catch (err) {}
+
+ callback(res, res_json, res.duration);
});
};
@@ -2553,10 +2554,16 @@
rpcBaseURL = Session.getLocalData('rpcBaseURL');
if (rpcBaseURL == null) {
+ var msg = {
+ jsonrpc: '2.0',
+ id: 'init',
+ method: 'list',
+ params: undefined
+ };
var rpcFallbackURL = this.url('admin/ubus');
- rpcBaseURL = Request.get(env.ubuspath).then(function(res) {
- return (rpcBaseURL = (res.status == 400) ? env.ubuspath : rpcFallbackURL);
+ rpcBaseURL = Request.post(env.ubuspath, msg, { nobatch: true }).then(function(res) {
+ return (rpcBaseURL = res.status == 200 ? env.ubuspath : rpcFallbackURL);
}, function() {
return (rpcBaseURL = rpcFallbackURL);
}).then(function(url) {
@@ -2965,7 +2972,12 @@
}).filter(function(e) {
return (e[1] != null);
}).sort(function(a, b) {
- return (a[1] > b[1]);
+ if (a[1] < b[1])
+ return -1;
+ else if (a[1] > b[1])
+ return 1;
+ else
+ return 0;
}).map(function(e) {
return e[0];
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js
index 8d825d73a0..17dd055e25 100644
--- a/modules/luci-base/htdocs/luci-static/resources/network.js
+++ b/modules/luci-base/htdocs/luci-static/resources/network.js
@@ -101,6 +101,15 @@ var _init = null,
_protocols = {},
_protospecs = {};
+function strcmp(a, b) {
+ if (a > b)
+ return 1;
+ else if (a < b)
+ return -1;
+ else
+ return 0;
+}
+
function getProtocolHandlers(cache) {
return callNetworkProtoHandlers().then(function(protos) {
/* Register "none" protocol */
@@ -384,12 +393,15 @@ function initNetworkState(refresh) {
name: name,
rawname: name,
flags: dev.flags,
+ link: dev.link,
stats: dev.stats,
macaddr: dev.mac,
type: dev.type,
+ devtype: dev.devtype,
mtu: dev.mtu,
qlen: dev.qlen,
wireless: dev.wireless,
+ parent: dev.parent,
ipaddrs: [],
ip6addrs: []
};
@@ -430,6 +442,16 @@ function initNetworkState(refresh) {
s.isBridge[name] = true;
}
+ for (var name in luci_devs) {
+ var dev = luci_devs[name];
+
+ if (!dev.parent || dev.devtype != 'dsa')
+ continue;
+
+ s.isSwitch[dev.parent] = true;
+ s.isSwitch[name] = true;
+ }
+
if (L.isObject(board_json.switch)) {
for (var switchname in board_json.switch) {
var layout = board_json.switch[switchname],
@@ -540,7 +562,7 @@ function ifnameOf(obj) {
}
function networkSort(a, b) {
- return a.getName() > b.getName();
+ return strcmp(a.getName(), b.getName());
}
function deviceSort(a, b) {
@@ -551,7 +573,7 @@ function deviceSort(a, b) {
if (weightA != weightB)
return weightA - weightB;
- return a.getName() > b.getName();
+ return strcmp(a.getName(), b.getName());
}
function formatWifiEncryption(enc) {
@@ -1209,6 +1231,59 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
}
}
+ /* find bridge VLAN devices */
+ var uciBridgeVLANs = uci.sections('network', 'bridge-vlan');
+ for (var i = 0; i < uciBridgeVLANs.length; i++) {
+ var basedev = uciBridgeVLANs[i].device,
+ local = uciBridgeVLANs[i].local,
+ alias = uciBridgeVLANs[i].alias,
+ vid = +uciBridgeVLANs[i].vlan,
+ ports = L.toArray(uciBridgeVLANs[i].ports);
+
+ if (local == '0')
+ continue;
+
+ if (isNaN(vid) || vid < 0 || vid > 4095)
+ continue;
+
+ var vlandev = '%s.%s'.format(basedev, alias || vid);
+
+ _state.isBridge[basedev] = true;
+
+ if (!_state.bridges.hasOwnProperty(basedev))
+ _state.bridges[basedev] = {
+ name: basedev,
+ ifnames: []
+ };
+
+ if (!devices.hasOwnProperty(vlandev))
+ devices[vlandev] = this.instantiateDevice(vlandev);
+
+ ports.forEach(function(port_name) {
+ var m = port_name.match(/^([^:]+)(?::[ut*]+)?$/),
+ p = m ? m[1] : null;
+
+ if (!p)
+ return;
+
+ if (_state.bridges[basedev].ifnames.filter(function(sd) { return sd.name == p }).length)
+ return;
+
+ _state.netdevs[p] = _state.netdevs[p] || {
+ name: p,
+ ipaddrs: [],
+ ip6addrs: [],
+ type: 1,
+ devtype: 'ethernet',
+ stats: {},
+ flags: {}
+ };
+
+ _state.bridges[basedev].ifnames.push(_state.netdevs[p]);
+ _state.netdevs[p].bridge = _state.bridges[basedev];
+ });
+ }
+
/* find wireless interfaces */
var uciWifiIfaces = uci.sections('wireless', 'wifi-iface'),
networkCount = {};
@@ -1224,6 +1299,22 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
devices[netid] = this.instantiateDevice(netid);
}
+ /* find uci declared devices */
+ var uciDevices = uci.sections('network', 'device');
+
+ for (var i = 0; i < uciDevices.length; i++) {
+ var type = uciDevices[i].type,
+ name = uciDevices[i].name;
+
+ if (!type || !name || devices.hasOwnProperty(name))
+ continue;
+
+ if (type == 'bridge')
+ _state.isBridge[name] = true;
+
+ devices[name] = this.instantiateDevice(name);
+ }
+
var rv = [];
for (var netdev in devices)
@@ -1340,7 +1431,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
rv.push(this.lookupWifiNetwork(wifiIfaces[i]['.name']));
rv.sort(function(a, b) {
- return (a.getID() > b.getID());
+ return strcmp(a.getID(), b.getID());
});
return rv;
@@ -1437,6 +1528,13 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
}
}
+ rv.sort(function(a, b) {
+ if (a.metric != b.metric)
+ return (a.metric - b.metric);
+
+ return strcmp(a.interface, b.interface);
+ });
+
return rv;
}, this));
},
@@ -1712,7 +1810,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getHostnameByMACAddr: function(mac) {
- return this.hosts[mac] ? this.hosts[mac].name : null;
+ return this.hosts[mac]
+ ? (this.hosts[mac].name || null)
+ : null;
},
/**
@@ -1727,7 +1827,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getIPAddrByMACAddr: function(mac) {
- return this.hosts[mac] ? this.hosts[mac].ipv4 : null;
+ return this.hosts[mac]
+ ? (L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4)[0] || null)
+ : null;
},
/**
@@ -1742,7 +1844,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getIP6AddrByMACAddr: function(mac) {
- return this.hosts[mac] ? this.hosts[mac].ipv6 : null;
+ return this.hosts[mac]
+ ? (L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6)[0] || null)
+ : null;
},
/**
@@ -1757,9 +1861,17 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getHostnameByIPAddr: function(ipaddr) {
- for (var mac in this.hosts)
- if (this.hosts[mac].ipv4 == ipaddr && this.hosts[mac].name != null)
- return this.hosts[mac].name;
+ for (var mac in this.hosts) {
+ if (this.hosts[mac].name == null)
+ continue;
+
+ var addrs = L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4);
+
+ for (var i = 0; i < addrs.length; i++)
+ if (addrs[i] == ipaddr)
+ return this.hosts[mac].name;
+ }
+
return null;
},
@@ -1775,16 +1887,21 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getMACAddrByIPAddr: function(ipaddr) {
- for (var mac in this.hosts)
- if (this.hosts[mac].ipv4 == ipaddr)
- return mac;
+ for (var mac in this.hosts) {
+ var addrs = L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4);
+
+ for (var i = 0; i < addrs.length; i++)
+ if (addrs[i] == ipaddr)
+ return mac;
+ }
+
return null;
},
/**
* Lookup the hostname associated with the given IPv6 address.
*
- * @param {string} ipaddr
+ * @param {string} ip6addr
* The IPv6 address to lookup.
*
* @returns {null|string}
@@ -1793,16 +1910,24 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getHostnameByIP6Addr: function(ip6addr) {
- for (var mac in this.hosts)
- if (this.hosts[mac].ipv6 == ip6addr && this.hosts[mac].name != null)
- return this.hosts[mac].name;
+ for (var mac in this.hosts) {
+ if (this.hosts[mac].name == null)
+ continue;
+
+ var addrs = L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6);
+
+ for (var i = 0; i < addrs.length; i++)
+ if (addrs[i] == ip6addr)
+ return this.hosts[mac].name;
+ }
+
return null;
},
/**
* Lookup the MAC address associated with the given IPv6 address.
*
- * @param {string} ipaddr
+ * @param {string} ip6addr
* The IPv6 address to lookup.
*
* @returns {null|string}
@@ -1811,9 +1936,14 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
* the corresponding host.
*/
getMACAddrByIP6Addr: function(ip6addr) {
- for (var mac in this.hosts)
- if (this.hosts[mac].ipv6 == ip6addr)
- return mac;
+ for (var mac in this.hosts) {
+ var addrs = L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6);
+
+ for (var i = 0; i < addrs.length; i++)
+ if (addrs[i] == ip6addr)
+ return mac;
+ }
+
return null;
},
@@ -1840,14 +1970,18 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
*/
getMACHints: function(preferIp6) {
var rv = [];
+
for (var mac in this.hosts) {
var hint = this.hosts[mac].name ||
- this.hosts[mac][preferIp6 ? 'ipv6' : 'ipv4'] ||
- this.hosts[mac][preferIp6 ? 'ipv4' : 'ipv6'];
+ L.toArray(this.hosts[mac][preferIp6 ? 'ip6addrs' : 'ipaddrs'] || this.hosts[mac][preferIp6 ? 'ipv6' : 'ipv4'])[0] ||
+ L.toArray(this.hosts[mac][preferIp6 ? 'ipaddrs' : 'ip6addrs'] || this.hosts[mac][preferIp6 ? 'ipv4' : 'ipv6'])[0];
rv.push([mac, hint]);
}
- return rv.sort(function(a, b) { return a[0] > b[0] });
+
+ return rv.sort(function(a, b) {
+ return strcmp(a[0], b[0]);
+ });
}
});
@@ -2403,14 +2537,14 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
*
* Alias interfaces are interfaces layering on top of another interface
* and are denoted by a special `@interfacename` notation in the
- * underlying `ifname` option.
+ * underlying `device` option.
*
* @returns {null|string}
* Returns the name of the parent interface if this logical interface
* is an alias or `null` if it is not an alias interface.
*/
isAlias: function() {
- var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')),
+ var ifnames = L.toArray(uci.get('network', this.sid, 'device')),
parent = null;
for (var i = 0; i < ifnames.length; i++)
@@ -2434,9 +2568,9 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
return false;
var empty = true,
- ifname = this._get('ifname');
+ device = this._get('device');
- if (ifname != null && ifname.match(/\S+/))
+ if (device != null && device.match(/\S+/))
empty = false;
if (empty == true && getWifiNetidBySid(this.sid) != null)
@@ -2468,18 +2602,18 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* argument was invalid, if the device was already part of the logical
* interface or if the logical interface is virtual.
*/
- addDevice: function(ifname) {
- ifname = ifnameOf(ifname);
+ addDevice: function(device) {
+ device = ifnameOf(device);
- if (ifname == null || this.isFloating())
+ if (device == null || this.isFloating())
return false;
- var wif = getWifiSidByIfname(ifname);
+ var wif = getWifiSidByIfname(device);
if (wif != null)
return appendValue('wireless', wif, 'network', this.sid);
- return appendValue('network', this.sid, 'ifname', ifname);
+ return appendValue('network', this.sid, 'device', device);
},
/**
@@ -2495,20 +2629,20 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* argument was invalid, if the device was already part of the logical
* interface or if the logical interface is virtual.
*/
- deleteDevice: function(ifname) {
+ deleteDevice: function(device) {
var rv = false;
- ifname = ifnameOf(ifname);
+ device = ifnameOf(device);
- if (ifname == null || this.isFloating())
+ if (device == null || this.isFloating())
return false;
- var wif = getWifiSidByIfname(ifname);
+ var wif = getWifiSidByIfname(device);
if (wif != null)
rv = removeValue('wireless', wif, 'network', this.sid);
- if (removeValue('network', this.sid, 'ifname', ifname))
+ if (removeValue('network', this.sid, 'device', device))
rv = true;
return rv;
@@ -2534,7 +2668,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
return new Device(ifname, this);
}
else {
- var ifnames = L.toArray(uci.get('network', this.sid, 'ifname'));
+ var ifnames = L.toArray(uci.get('network', this.sid, 'device'));
for (var i = 0; i < ifnames.length; i++) {
var m = ifnames[i].match(/^([^:/]+)/);
@@ -2589,13 +2723,10 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
if (!this.isBridge() && !(this.isVirtual() && !this.isFloating()))
return null;
- var ifnames = L.toArray(uci.get('network', this.sid, 'ifname'));
-
- for (var i = 0; i < ifnames.length; i++) {
- if (ifnames[i].charAt(0) == '@')
- continue;
+ var device = uci.get('network', this.sid, 'device');
- var m = ifnames[i].match(/^([^:/]+)/);
+ if (device && device.charAt(0) != '@') {
+ var m = device.match(/^([^:/]+)/);
if (m != null)
rv.push(Network.prototype.instantiateDevice(m[1], this));
}
@@ -2637,25 +2768,24 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* Returns `true` when this logical interface contains the given network
* device or `false` if not.
*/
- containsDevice: function(ifname) {
- ifname = ifnameOf(ifname);
+ containsDevice: function(device) {
+ device = ifnameOf(device);
- if (ifname == null)
+ if (device == null)
return false;
- else if (this.isVirtual() && '%s-%s'.format(this.getProtocol(), this.sid) == ifname)
+ else if (this.isVirtual() && '%s-%s'.format(this.getProtocol(), this.sid) == device)
return true;
- else if (this.isBridge() && 'br-%s'.format(this.sid) == ifname)
+ else if (this.isBridge() && 'br-%s'.format(this.sid) == device)
return true;
- var ifnames = L.toArray(uci.get('network', this.sid, 'ifname'));
-
- for (var i = 0; i < ifnames.length; i++) {
- var m = ifnames[i].match(/^([^:/]+)/);
- if (m != null && m[1] == ifname)
+ var name = uci.get('network', this.sid, 'device');
+ if (name) {
+ var m = name.match(/^([^:/]+)/);
+ if (m != null && m[1] == device)
return true;
}
- var wif = getWifiSidByIfname(ifname);
+ var wif = getWifiSidByIfname(device);
if (wif != null) {
var networks = L.toArray(uci.get('wireless', wif, 'network'));
@@ -2698,19 +2828,19 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* device and allows querying device details such as packet statistics or MTU.
*/
Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
- __init__: function(ifname, network) {
- var wif = getWifiSidByIfname(ifname);
+ __init__: function(device, network) {
+ var wif = getWifiSidByIfname(device);
if (wif != null) {
var res = getWifiStateBySid(wif) || [],
netid = getWifiNetidBySid(wif) || [];
- this.wif = new WifiNetwork(wif, res[0], res[1], netid[0], res[2], { ifname: ifname });
- this.ifname = this.wif.getIfname();
+ this.wif = new WifiNetwork(wif, res[0], res[1], netid[0], res[2], { ifname: device });
+ this.device = this.wif.getIfname();
}
- this.ifname = this.ifname || ifname;
- this.dev = _state.netdevs[this.ifname];
+ this.device = this.device || device;
+ this.dev = Object.assign({}, _state.netdevs[this.device]);
this.network = network;
},
@@ -2733,7 +2863,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* Returns the name of the device, e.g. `eth0` or `wlan0`.
*/
getName: function() {
- return (this.wif != null ? this.wif.getIfname() : this.ifname);
+ return (this.wif != null ? this.wif.getIfname() : this.device);
},
/**
@@ -2781,7 +2911,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
},
/**
- * Get the type of the device..
+ * Get the type of the device.
*
* @returns {string}
* Returns a string describing the type of the network device:
@@ -2794,17 +2924,17 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* - `ethernet` for all other device types
*/
getType: function() {
- if (this.ifname != null && this.ifname.charAt(0) == '@')
+ if (this.device != null && this.device.charAt(0) == '@')
return 'alias';
- else if (this.wif != null || isWifiIfname(this.ifname))
+ else if (this.dev.devtype == 'wlan' || this.wif != null || isWifiIfname(this.device))
return 'wifi';
- else if (_state.isBridge[this.ifname])
+ else if (this.dev.devtype == 'bridge' || _state.isBridge[this.device])
return 'bridge';
- else if (_state.isTunnel[this.ifname])
+ else if (_state.isTunnel[this.device])
return 'tunnel';
- else if (this.ifname.indexOf('.') > -1)
+ else if (this.dev.devtype == 'vlan' || this.device.indexOf('.') > -1)
return 'vlan';
- else if (_state.isSwitch[this.ifname])
+ else if (this.dev.devtype == 'dsa' || _state.isSwitch[this.device])
return 'switch';
else
return 'ethernet';
@@ -2821,7 +2951,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
if (this.wif != null)
return this.wif.getShortName();
- return this.ifname;
+ return this.device;
},
/**
@@ -2861,10 +2991,11 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
return _('Bridge');
case 'switch':
- return _('Ethernet Switch');
+ return (_state.netdevs[this.device] && _state.netdevs[this.device].devtype == 'dsa')
+ ? _('Switch port') : _('Ethernet Switch');
case 'vlan':
- return (_state.isSwitch[this.ifname] ? _('Switch VLAN') : _('Software VLAN'));
+ return (_state.isSwitch[this.device] ? _('Switch VLAN') : _('Software VLAN'));
case 'tunnel':
return _('Tunnel Interface');
@@ -2883,7 +3014,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* a Linux bridge.
*/
getPorts: function() {
- var br = _state.bridges[this.ifname],
+ var br = _state.bridges[this.device],
rv = [];
if (br == null || !Array.isArray(br.ifnames))
@@ -2905,7 +3036,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* device is not a Linux bridge.
*/
getBridgeID: function() {
- var br = _state.bridges[this.ifname];
+ var br = _state.bridges[this.device];
return (br != null ? br.id : null);
},
@@ -2917,7 +3048,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* enabled, else `false`.
*/
getBridgeSTP: function() {
- var br = _state.bridges[this.ifname];
+ var br = _state.bridges[this.device];
return (br != null ? !!br.stp : false);
},
@@ -3004,6 +3135,47 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
},
/**
+ * Get the carrier state of the network device.
+ *
+ * @returns {boolean}
+ * Returns true if the device has a carrier, e.g. when a cable is
+ * inserted into an ethernet port of false if there is none.
+ */
+ getCarrier: function() {
+ var link = this._devstate('link');
+ return (link != null ? link.carrier || false : false);
+ },
+
+ /**
+ * Get the current link speed of the network device if available.
+ *
+ * @returns {number|null}
+ * Returns the current speed of the network device in Mbps. If the
+ * device supports no ethernet speed levels, null is returned.
+ * If the device supports ethernet speeds but has no carrier, -1 is
+ * returned.
+ */
+ getSpeed: function() {
+ var link = this._devstate('link');
+ return (link != null ? link.speed || null : null);
+ },
+
+ /**
+ * Get the current duplex mode of the network device if available.
+ *
+ * @returns {string|null}
+ * Returns the current duplex mode of the network device. Returns
+ * either "full" or "half" if the device supports duplex modes or
+ * null if the duplex mode is unknown or unsupported.
+ */
+ getDuplex: function() {
+ var link = this._devstate('link'),
+ duplex = link ? link.duplex : null;
+
+ return (duplex != 'unknown') ? duplex : null;
+ },
+
+ /**
* Get the primary logical interface this device is assigned to.
*
* @returns {null|LuCI.network.Protocol}
@@ -3029,7 +3201,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
var networks = enumerateNetworks.apply(L.network);
for (var i = 0; i < networks.length; i++)
- if (networks[i].containsDevice(this.ifname) || networks[i].getIfname() == this.ifname)
+ if (networks[i].containsDevice(this.device) || networks[i].getIfname() == this.device)
this.networks.push(networks[i]);
this.networks.sort(networkSort);
@@ -3048,6 +3220,22 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
*/
getWifiNetwork: function() {
return (this.wif != null ? this.wif : null);
+ },
+
+ /**
+ * Get the logical parent device of this device.
+ *
+ * In case of DSA switch ports, the parent device will be the DSA switch
+ * device itself, for VLAN devices, the parent refers to the base device
+ * etc.
+ *
+ * @returns {null|LuCI.network.Device}
+ * Returns a `Network.Device` instance representing the parent device or
+ * `null` when this device has no parent, as it is the case for e.g.
+ * ordinary ethernet interfaces.
+ */
+ getParent: function() {
+ return this.dev.parent ? Network.prototype.instantiateDevice(this.dev.parent) : null;
}
});
@@ -3157,6 +3345,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
* - `g` - Legacy 802.11g mode, 2.4 GHz, up to 54 Mbit/s
* - `n` - IEEE 802.11n mode, 2.4 or 5 GHz, up to 600 Mbit/s
* - `ac` - IEEE 802.11ac mode, 5 GHz, up to 6770 Mbit/s
+ * - `ax` - IEEE 802.11ax mode, 2.4 or 5 GHz
*/
getHWModes: function() {
var hwmodes = this.ubus('dev', 'iwinfo', 'hwmodes');
@@ -3178,6 +3367,10 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
* - `VHT40` - applicable to IEEE 802.11ac, 40 MHz wide channels
* - `VHT80` - applicable to IEEE 802.11ac, 80 MHz wide channels
* - `VHT160` - applicable to IEEE 802.11ac, 160 MHz wide channels
+ * - `HE20` - applicable to IEEE 802.11ax, 20 MHz wide channels
+ * - `HE40` - applicable to IEEE 802.11ax, 40 MHz wide channels
+ * - `HE80` - applicable to IEEE 802.11ax, 80 MHz wide channels
+ * - `HE160` - applicable to IEEE 802.11ax, 160 MHz wide channels
*/
getHTModes: function() {
var htmodes = this.ubus('dev', 'iwinfo', 'htmodes');
@@ -3201,7 +3394,10 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
modestr = '';
hwmodes.sort(function(a, b) {
- return (a.length != b.length ? a.length > b.length : a > b);
+ if (a.length != b.length)
+ return a.length - b.length;
+
+ return strcmp(a, b);
});
modestr = hwmodes.join('');
@@ -3555,6 +3751,24 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
},
/**
+ * Get the Linux VLAN network device names.
+ *
+ * @returns {string[]}
+ * Returns the current Linux VLAN network device name as resolved
+ * from `ubus` runtime information or empty array if this network
+ * has no associated VLAN network devices.
+ */
+ getVlanIfnames: function() {
+ var vlans = L.toArray(this.ubus('net', 'vlans')),
+ ifnames = [];
+
+ for (var i = 0; i < vlans.length; i++)
+ ifnames.push(vlans[i]['ifname']);
+
+ return ifnames;
+ },
+
+ /**
* Get the name of the corresponding wifi radio device.
*
* @returns {null|string}
@@ -3854,6 +4068,17 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* @property {number} [nss]
* Specifies the number of spatial streams used by the transmission.
* Only applicable to VHT rates.
+ *
+ * @property {boolean} [he]
+ * Specifies whether this rate is an HE (IEEE 802.11ax) rate.
+ *
+ * @property {number} [he_gi]
+ * Specifies whether the guard interval used for the transmission.
+ * Only applicable to HE rates.
+ *
+ * @property {number} [he_dcm]
+ * Specifies whether dual concurrent modulation is used for the transmission.
+ * Only applicable to HE rates.
*/
/**
@@ -3864,7 +4089,15 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* with this network.
*/
getAssocList: function() {
- return callIwinfoAssoclist(this.getIfname());
+ var tasks = [];
+ var ifnames = [ this.getIfname() ].concat(this.getVlanIfnames());
+
+ for (var i = 0; i < ifnames.length; i++)
+ tasks.push(callIwinfoAssoclist(ifnames[i]));
+
+ return Promise.all(tasks).then(function(values) {
+ return Array.prototype.concat.apply([], values);
+ });
},
/**
diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
index f0a3ec579c..71adc235ca 100644
--- a/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
+++ b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
@@ -17,10 +17,13 @@ return network.registerProtocol('dhcp', {
},
renderFormOptions: function(s) {
- var dev = this.getL2Device() || this.getDevice(), o;
+ var o;
o = s.taboption('general', form.Value, 'hostname', _('Hostname to send when requesting DHCP'));
- o.datatype = 'hostname';
+ o.default = '';
+ o.value('', _('Send the hostname of this device'));
+ o.value('*', _('Do not send a hostname'));
+ o.datatype = 'or(hostname, "*")';
o.load = function(section_id) {
return callFileRead('/proc/sys/kernel/hostname').then(L.bind(function(hostname) {
this.placeholder = hostname;
@@ -31,32 +34,9 @@ return network.registerProtocol('dhcp', {
o = s.taboption('advanced', form.Flag, 'broadcast', _('Use broadcast flag'), _('Required for certain ISPs, e.g. Charter with DOCSIS 3'));
o.default = o.disabled;
- o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
- o.default = o.enabled;
-
- o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
- o.default = o.enabled;
-
- o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
- o.depends('peerdns', '0');
- o.datatype = 'ipaddr';
- o.cast = 'string';
-
- o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
- o.placeholder = '0';
- o.datatype = 'uinteger';
-
o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP'));
o.datatype = 'hexstring';
s.taboption('advanced', form.Value, 'vendorid', _('Vendor Class to send when requesting DHCP'));
-
- o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address'));
- o.datatype = 'macaddr';
- o.placeholder = dev ? (dev.getMAC() || '') : '';
-
- o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
- o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
- o.datatype = 'max(9200)';
}
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/static.js b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js
index 2d70ae681f..42ebcefba4 100644
--- a/modules/luci-base/htdocs/luci-static/resources/protocol/static.js
+++ b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js
@@ -173,34 +173,12 @@ return network.registerProtocol('static', {
},
renderFormOptions: function(s) {
- var dev = this.getL2Device() || this.getDevice(), o;
+ var o;
s.taboption('general', this.CBIIPValue, 'ipaddr', _('IPv4 address'));
s.taboption('general', this.CBINetmaskValue, 'netmask', _('IPv4 netmask'));
s.taboption('general', this.CBIGatewayValue, 'gateway', _('IPv4 gateway'));
s.taboption('general', this.CBIBroadcastValue, 'broadcast', _('IPv4 broadcast'));
- s.taboption('general', form.DynamicList, 'dns', _('Use custom DNS servers'));
-
- o = s.taboption('general', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
- o.value('', _('disabled'));
- o.value('64');
- o.datatype = 'max(64)';
-
- o = s.taboption('general', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
- o.placeholder = '0';
- o.validate = function(section_id, value) {
- if (value == null || value == '')
- return true;
-
- var n = parseInt(value, 16);
-
- if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff)
- return _('Expecting a hexadecimal assignment hint');
-
- return true;
- };
- for (var i = 33; i <= 64; i++)
- o.depends('ip6assign', String(i));
o = s.taboption('general', form.DynamicList, 'ip6addr', _('IPv6 address'));
o.datatype = 'ip6addr';
@@ -214,21 +192,5 @@ return network.registerProtocol('static', {
o = s.taboption('general', form.Value, 'ip6prefix', _('IPv6 routed prefix'), _('Public prefix routed to this device for distribution to clients.'));
o.datatype = 'ip6addr';
o.depends('ip6assign', '');
-
- o = s.taboption('general', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface."));
- o.datatype = 'ip6hostid';
- o.placeholder = '::1';
-
- o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address'));
- o.datatype = 'macaddr';
- o.placeholder = dev ? (dev.getMAC() || '') : '';
-
- o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
- o.datatype = 'max(9200)';
- o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
-
- o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
- o.placeholder = this.getMetric() || '0';
- o.datatype = 'uinteger';
}
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js
index 7bfc913367..f37f7bb6a4 100644
--- a/modules/luci-base/htdocs/luci-static/resources/rpc.js
+++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js
@@ -33,9 +33,6 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
req[i].params[2]
);
}
- else if (req.params) {
- q += '/%s.%s'.format(req.params[1], req.params[2]);
- }
return request.post(rpcBaseURL + q, req, {
timeout: (L.env.rpctimeout || 20) * 1000,
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/prng.js b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js
index 752dc75ce8..b916cc7792 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/prng.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js
@@ -89,5 +89,23 @@ return L.Class.extend({
}
return Math.floor(r * (u - l + 1)) + l;
+ },
+
+ derive_color: function(string) {
+ this.seed(parseInt(sfh(string), 16));
+
+ var r = this.get(128),
+ g = this.get(128),
+ min = 0,
+ max = 128;
+
+ if ((r + g) < 128)
+ min = 128 - r - g;
+ else
+ max = 255 - r - g;
+
+ var b = min + Math.floor(this.get() * (max - min));
+
+ return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b);
}
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
index 7f724a17e4..3f2b7b9ffc 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -393,6 +393,7 @@ var CBINetworkSelect = form.ListValue.extend({
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
+ datatype: this.multiple ? 'list(uciname)' : 'uciname',
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
@@ -580,6 +581,8 @@ var CBIUserSelect = form.ListValue.extend({
load: function(section_id) {
return getUsers().then(L.bind(function(users) {
+ delete this.keylist;
+ delete this.vallist;
for (var i = 0; i < users.length; i++) {
this.value(users[i]);
}
diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js
index 640661f0f5..41e902c5fe 100644
--- a/modules/luci-base/htdocs/luci-static/resources/uci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/uci.js
@@ -402,7 +402,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
for (var s in v)
if (!d || d[s] !== true)
if (!type || v[s]['.type'] == type)
- sa.push(Object.assign({ }, v[s], c ? c[s] : undefined));
+ sa.push(Object.assign({ }, v[s], c ? c[s] : null));
if (n)
for (var s in n)
@@ -462,7 +462,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/* requested option in a just created section */
if (n[conf] && n[conf][sid]) {
if (!n[conf])
- return undefined;
+ return null;
if (opt == null)
return n[conf][sid];
@@ -473,14 +473,9 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/* requested an option value */
if (opt != null) {
/* check whether option was deleted */
- if (d[conf] && d[conf][sid]) {
- if (d[conf][sid] === true)
- return undefined;
-
- for (var i = 0; i < d[conf][sid].length; i++)
- if (d[conf][sid][i] == opt)
- return undefined;
- }
+ if (d[conf] && d[conf][sid])
+ if (d[conf][sid] === true || d[conf][sid][opt])
+ return null;
/* check whether option was changed */
if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
@@ -490,14 +485,34 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
if (v[conf] && v[conf][sid])
return v[conf][sid][opt];
- return undefined;
+ return null;
}
/* requested an entire section */
- if (v[conf])
- return v[conf][sid];
+ if (v[conf]) {
+ /* check whether entire section was deleted */
+ if (d[conf] && d[conf][sid] === true)
+ return null;
+
+ var s = v[conf][sid] || null;
+
+ if (s) {
+ /* merge changes */
+ if (c[conf] && c[conf][sid])
+ for (var opt in c[conf][sid])
+ if (c[conf][sid][opt] != null)
+ s[opt] = c[conf][sid][opt];
+
+ /* merge deletions */
+ if (d[conf] && d[conf][sid])
+ for (var opt in d[conf][sid])
+ delete s[opt];
+ }
- return undefined;
+ return s;
+ }
+
+ return null;
},
/**
@@ -555,28 +570,39 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/* undelete option */
if (d[conf] && d[conf][sid]) {
- d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });
+ var empty = true;
+
+ for (var key in d[conf][sid]) {
+ if (key != opt && d[conf][sid].hasOwnProperty(key)) {
+ empty = false;
+ break;
+ }
+ }
- if (d[conf][sid].length == 0)
+ if (empty)
delete d[conf][sid];
+ else
+ delete d[conf][sid][opt];
}
c[conf][sid][opt] = val;
}
else {
- /* only delete in existing sections */
- if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) &&
- !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt)))
- return;
+ /* revert any change for to-be-deleted option */
+ if (c[conf] && c[conf][sid])
+ delete c[conf][sid][opt];
- if (!d[conf])
- d[conf] = { };
+ /* only delete existing options */
+ if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) {
+ if (!d[conf])
+ d[conf] = { };
- if (!d[conf][sid])
- d[conf][sid] = [ ];
+ if (!d[conf][sid])
+ d[conf][sid] = { };
- if (d[conf][sid] !== true)
- d[conf][sid].push(opt);
+ if (d[conf][sid] !== true)
+ d[conf][sid][opt] = true;
+ }
}
},
@@ -796,7 +822,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
for (var conf in d) {
for (var sid in d[conf]) {
var o = d[conf][sid];
- tasks.push(self.callDelete(conf, sid, (o === true) ? null : o));
+
+ if (o === true)
+ tasks.push(self.callDelete(conf, sid, null));
+ else
+ tasks.push(self.callDelete(conf, sid, Object.keys(o)));
}
pkgs[conf] = true;
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index 91a93b9e90..0e909b6dcc 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -102,6 +102,47 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
},
/**
+ * 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
@@ -569,6 +610,22 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
frameEl.appendChild(E('label', { 'for': id }));
+ if (this.options.tooltip != null) {
+ var icon = "⚠️";
+
+ if (this.options.tooltipicon != null)
+ icon = this.options.tooltipicon;
+
+ frameEl.appendChild(
+ E('label', { 'class': 'cbi-tooltip-container' },[
+ icon,
+ E('div', { 'class': 'cbi-tooltip' },
+ this.options.tooltip
+ )
+ ])
+ );
+ }
+
return this.bind(frameEl);
},
@@ -576,8 +633,9 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
bind: function(frameEl) {
this.node = frameEl;
- this.setUpdateEvents(frameEl.lastElementChild.previousElementSibling, 'click', 'blur');
- this.setChangeEvents(frameEl.lastElementChild.previousElementSibling, 'change');
+ var input = frameEl.querySelector('input[type="checkbox"]');
+ this.setUpdateEvents(input, 'click', 'blur');
+ this.setChangeEvents(input, 'change');
dom.bindClassInstance(frameEl, this);
@@ -593,7 +651,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
* Returns `true` when the checkbox is currently checked, otherwise `false`.
*/
isChecked: function() {
- return this.node.lastElementChild.previousElementSibling.checked;
+ return this.node.querySelector('input[type="checkbox"]').checked;
},
/** @override */
@@ -605,7 +663,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
/** @override */
setValue: function(value) {
- this.node.lastElementChild.previousElementSibling.checked = (value == this.options.value_enabled);
+ this.node.querySelector('input[type="checkbox"]').checked = (value == this.options.value_enabled);
}
});
@@ -1144,6 +1202,28 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
},
/** @private */
+ getScrollParent: function(element) {
+ var parent = element,
+ style = getComputedStyle(element),
+ excludeStaticParent = (style.position === 'absolute');
+
+ if (style.position === 'fixed')
+ return document.body;
+
+ while ((parent = parent.parentElement) != null) {
+ style = getComputedStyle(parent);
+
+ if (excludeStaticParent && style.position === 'static')
+ continue;
+
+ if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX))
+ return parent;
+ }
+
+ return document.body;
+ },
+
+ /** @private */
openDropdown: function(sb) {
var st = window.getComputedStyle(sb, null),
ul = sb.querySelector('ul'),
@@ -1151,7 +1231,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
fl = findParent(sb, '.cbi-value-field'),
sel = ul.querySelector('[selected]'),
rect = sb.getBoundingClientRect(),
- items = Math.min(this.options.dropdown_items, li.length);
+ items = Math.min(this.options.dropdown_items, li.length),
+ scrollParent = this.getScrollParent(sb);
document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
@@ -1176,29 +1257,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
ul.style.maxHeight = (vpHeight * 0.5) + 'px';
ul.style.WebkitOverflowScrolling = 'touch';
- var getScrollParent = function(element) {
- var parent = element,
- style = getComputedStyle(element),
- excludeStaticParent = (style.position === 'absolute');
-
- if (style.position === 'fixed')
- return document.body;
-
- while ((parent = parent.parentElement) != null) {
- style = getComputedStyle(parent);
-
- if (excludeStaticParent && style.position === 'static')
- continue;
-
- if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX))
- return parent;
- }
-
- return document.body;
- }
-
- var scrollParent = getScrollParent(sb),
- scrollFrom = scrollParent.scrollTop,
+ var scrollFrom = scrollParent.scrollTop,
scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
var scrollStep = function(timestamp) {
@@ -1224,10 +1283,11 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
ul.style.top = ul.style.bottom = '';
window.requestAnimationFrame(function() {
- var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
+ var containerRect = scrollParent.getBoundingClientRect(),
+ itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
fullHeight = 0,
- spaceAbove = rect.top,
- spaceBelow = window.innerHeight - rect.height - rect.top;
+ spaceAbove = rect.top - containerRect.top,
+ spaceBelow = containerRect.bottom - rect.bottom;
for (var i = 0; i < (items == -1 ? li.length : items); i++)
fullHeight += li[i].getBoundingClientRect().height;
@@ -1303,6 +1363,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
/** @private */
toggleItem: function(sb, li, force_state) {
+ var ul = li.parentNode;
+
if (li.hasAttribute('unselectable'))
return;
@@ -1379,7 +1441,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
this.closeDropdown(sb, true);
}
- this.saveValues(sb, li.parentNode);
+ this.saveValues(sb, ul);
},
/** @private */
@@ -3556,9 +3618,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
this.setActiveTabId(panes[selected], selected);
}
- panes[selected].dispatchEvent(new CustomEvent('cbi-tab-active', {
- detail: { tab: panes[selected].getAttribute('data-tab') }
- }));
+ requestAnimationFrame(L.bind(function(pane) {
+ pane.dispatchEvent(new CustomEvent('cbi-tab-active', {
+ detail: { tab: pane.getAttribute('data-tab') }
+ }));
+ }, this, panes[selected]));
this.updateTabs(group);
},
@@ -4001,7 +4065,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
E('button', {
'class': 'btn',
'click': UI.prototype.hideModal
- }, [ _('Dismiss') ]), ' ',
+ }, [ _('Close') ]), ' ',
E('button', {
'class': 'cbi-button cbi-button-positive important',
'click': L.bind(this.apply, this, true)
diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js
index d47392c239..28042ba8cd 100644
--- a/modules/luci-base/htdocs/luci-static/resources/validation.js
+++ b/modules/luci-base/htdocs/luci-static/resources/validation.js
@@ -272,18 +272,21 @@ var ValidatorFactory = baseclass.extend({
_('valid IPv6 prefix value (0-128)'));
},
- cidr: function() {
- return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('valid IPv4 or IPv6 CIDR'));
+ cidr: function(negative) {
+ return this.assert(this.apply('cidr4', null, [negative]) || this.apply('cidr6', null, [negative]),
+ _('valid IPv4 or IPv6 CIDR'));
},
- cidr4: function() {
- var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/);
- return this.assert(m && this.factory.parseIPv4(m[1]) && this.apply('ip4prefix', m[2]), _('valid IPv4 CIDR'));
+ cidr4: function(negative) {
+ var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/);
+ return this.assert(m && this.factory.parseIPv4(m[1]) && (negative || !m[2]) && this.apply('ip4prefix', m[3]),
+ _('valid IPv4 CIDR'));
},
- cidr6: function() {
- var m = this.value.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/);
- return this.assert(m && this.factory.parseIPv6(m[1]) && this.apply('ip6prefix', m[2]), _('valid IPv6 CIDR'));
+ cidr6: function(negative) {
+ var m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/);
+ return this.assert(m && this.factory.parseIPv6(m[1]) && (negative || !m[2]) && this.apply('ip6prefix', m[3]),
+ _('valid IPv6 CIDR'));
},
ipnet4: function() {
@@ -304,18 +307,18 @@ var ValidatorFactory = baseclass.extend({
return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id'));
},
- ipmask: function() {
- return this.assert(this.apply('ipmask4') || this.apply('ipmask6'),
+ ipmask: function(negative) {
+ return this.assert(this.apply('ipmask4', null, [negative]) || this.apply('ipmask6', null, [negative]),
_('valid network in address/netmask notation'));
},
- ipmask4: function() {
- return this.assert(this.apply('cidr4') || this.apply('ipnet4') || this.apply('ip4addr'),
+ ipmask4: function(negative) {
+ return this.assert(this.apply('cidr4', null, [negative]) || this.apply('ipnet4') || this.apply('ip4addr'),
_('valid IPv4 network'));
},
- ipmask6: function() {
- return this.assert(this.apply('cidr6') || this.apply('ipnet6') || this.apply('ip6addr'),
+ ipmask6: function(negative) {
+ return this.assert(this.apply('cidr6', null, [negative]) || this.apply('ipnet6') || this.apply('ip6addr'),
_('valid IPv6 network'));
},
@@ -358,8 +361,8 @@ var ValidatorFactory = baseclass.extend({
},
network: function() {
- return this.assert(this.apply('uciname') || this.apply('host'),
- _('valid UCI identifier, hostname or IP address'));
+ return this.assert(this.apply('uciname') || this.apply('hostname') || this.apply('ip4addr') || this.apply('ip6addr'),
+ _('valid UCI identifier, hostname or IP address range'));
},
hostport: function(ipv4only) {