summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs/luci-static
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs/luci-static')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js81
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js332
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/fs.js6
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/alias.pngbin652 -> 651 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/alias_disabled.pngbin391 -> 371 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/bridge.pngbin681 -> 646 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/bridge_disabled.pngbin405 -> 385 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/ethernet.pngbin701 -> 664 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/ethernet_disabled.pngbin399 -> 384 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/port_down.pngbin518 -> 489 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/port_up.pngbin586 -> 561 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-0-25.pngbin458 -> 451 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-0.pngbin435 -> 430 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-25-50.pngbin465 -> 454 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-50-75.pngbin467 -> 454 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-75-100.pngbin457 -> 440 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/signal-none.pngbin639 -> 624 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/switch.pngbin676 -> 656 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/switch_disabled.pngbin398 -> 388 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/tunnel.pngbin343 -> 339 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/tunnel_disabled.pngbin235 -> 234 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/vlan.pngbin676 -> 656 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/vlan_disabled.pngbin398 -> 388 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/wifi.pngbin767 -> 745 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/icons/wifi_disabled.pngbin494 -> 480 bytes
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js303
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/network.js128
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/rpc.js10
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js9
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/uci.js79
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js614
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/validation.js9
32 files changed, 1048 insertions, 523 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index 324a91403f..38687a1cef 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -370,12 +370,7 @@ function cbi_validate_form(form, errmsg)
function cbi_validate_named_section_add(input)
{
var button = input.parentNode.parentNode.querySelector('.cbi-button-add');
- if (input.value !== '') {
- button.disabled = false;
- }
- else {
- button.disabled = true;
- }
+ button.disabled = input.value === '';
}
function cbi_validate_reset(form)
@@ -764,72 +759,14 @@ function cbi_update_table(table, data, placeholder) {
if (!isElem(target))
return;
- target.querySelectorAll('tr.table-titles, .tr.table-titles, .cbi-section-table-titles').forEach(function(thead) {
- var titles = [];
-
- thead.querySelectorAll('th, .th').forEach(function(th) {
- titles.push(th);
- });
-
- if (Array.isArray(data)) {
- var n = 0, rows = target.querySelectorAll('tr, .tr'), trows = [];
-
- data.forEach(function(row) {
- 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('td', {
- 'class': titles[i].className,
- 'data-title': (text !== '') ? text : null
- }, (row[i] != null) ? row[i] : ''));
-
- td.classList.remove('th');
- td.classList.add('td');
- }
-
- trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
-
- trows[n] = trow;
- });
-
- for (var i = 1; i <= n; i++) {
- if (rows[i])
- target.replaceChild(trows[i], rows[i]);
- else
- target.appendChild(trows[i]);
- }
-
- while (rows[++n])
- target.removeChild(rows[n]);
-
- if (placeholder && target.firstElementChild === target.lastElementChild) {
- var trow = target.appendChild(E('tr', { 'class': 'tr placeholder' }));
- var td = trow.appendChild(E('td', { 'class': titles[0].className }, placeholder));
+ var t = L.dom.findClassInstance(target);
- td.classList.remove('th');
- td.classList.add('td');
- }
- }
- else {
- thead.parentNode.style.display = 'none';
-
- thead.parentNode.querySelectorAll('tr, .tr, .cbi-section-table-row').forEach(function(trow) {
- if (trow !== thead) {
- var n = 0;
- trow.querySelectorAll('th, td, .th, .td').forEach(function(td) {
- if (n < titles.length) {
- var text = (titles[n++].innerText || '').trim();
- if (text !== '')
- td.setAttribute('data-title', text);
- }
- });
- }
- });
+ if (!(t instanceof L.ui.Table)) {
+ t = new L.ui.Table(target);
+ L.dom.bindClassInstance(target, t);
+ }
- thead.parentNode.style.display = '';
- }
- });
+ t.update(data, placeholder);
}
function showModal(title, children)
@@ -854,5 +791,7 @@ document.addEventListener('DOMContentLoaded', function() {
L.hideTooltip(ev);
});
- document.querySelectorAll('.table').forEach(cbi_update_table);
+ L.require('ui').then(function(ui) {
+ document.querySelectorAll('.table').forEach(cbi_update_table);
+ });
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
index 23cc0b1cb5..66ba030208 100644
--- a/modules/luci-base/htdocs/luci-static/resources/form.js
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -74,7 +74,7 @@ var CBIJSONConfig = baseclass.extend({
if (indexA != indexB)
return (indexA - indexB);
- return (a > b);
+ return L.naturalCompare(a, b);
}, this));
for (var i = 0; i < section_ids.length; i++)
@@ -204,7 +204,7 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
/**
* Add another form element as children to this element.
*
- * @param {AbstractElement} element
+ * @param {AbstractElement} obj
* The form element to add.
*/
append: function(obj) {
@@ -217,7 +217,7 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
* The `parse()` function recursively walks the form element tree and
* triggers input value reading and validation for each encountered element.
*
- * Elements which are hidden due to unsatisified dependencies are skipped.
+ * Elements which are hidden due to unsatisfied dependencies are skipped.
*
* @returns {Promise<void>}
* Returns a promise resolving once this element's value and the values of
@@ -277,19 +277,23 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
/**
* Strip any HTML tags from the given input string.
*
- * @param {string} input
+ * @param {string} s
* The input string to clean.
*
* @returns {string}
- * The cleaned input string with HTML removes removed.
+ * The cleaned input string with HTML tags removed.
*/
stripTags: function(s) {
if (typeof(s) == 'string' && !s.match(/[<>]/))
return s;
- var x = dom.parse('<div>' + s + '</div>');
+ var x = dom.elem(s) ? s : dom.parse('<div>' + s + '</div>');
- return x.textContent || x.innerText || '';
+ x.querySelectorAll('br').forEach(function(br) {
+ x.replaceChild(document.createTextNode('\n'), br);
+ });
+
+ return (x.textContent || x.innerText || '').replace(/([ \t]*\n)+/g, '\n');
},
/**
@@ -344,7 +348,7 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
* @classdesc
*
* The `Map` class represents one complete form. A form usually maps one UCI
- * configuraton file and is divided into multiple sections containing multiple
+ * configuration file and is divided into multiple sections containing multiple
* fields each.
*
* It serves as main entry point into the `LuCI.form` for typical view code.
@@ -360,7 +364,7 @@ var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.p
*
* @param {string} [description]
* The description text of the form which is usually rendered as text
- * paragraph below the form title and before the actual form conents.
+ * paragraph below the form title and before the actual form contents.
* If omitted, the corresponding paragraph element will not be rendered.
*/
var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
@@ -498,11 +502,11 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
* @param {LuCI.form.AbstractSection} sectionclass
* The section class to use for rendering the configuration section.
* Note that this value must be the class itself, not a class instance
- * obtained from calling `new`. It must also be a class dervied from
+ * obtained from calling `new`. It must also be a class derived from
* `LuCI.form.AbstractSection`.
*
* @param {...string} classargs
- * Additional arguments which are passed as-is to the contructor of the
+ * Additional arguments which are passed as-is to the constructor of the
* given section class. Refer to the class specific constructor
* documentation for details.
*
@@ -554,7 +558,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
* The `parse()` function recursively walks the form element tree and
* triggers input value reading and validation for each child element.
*
- * Elements which are hidden due to unsatisified dependencies are skipped.
+ * Elements which are hidden due to unsatisfied dependencies are skipped.
*
* @returns {Promise<void>}
* Returns a promise resolving once the entire form completed parsing all
@@ -584,7 +588,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
*
* @param {boolean} [silent=false]
* If set to `true`, trigger an alert message to the user in case saving
- * the form data failes. Otherwise fail silently.
+ * the form data failures. Otherwise fail silently.
*
* @returns {Promise<void>}
* Returns a promise resolving once the entire save operation is complete.
@@ -684,15 +688,15 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
/**
* Find a form option element instance.
*
- * @param {string} name_or_id
+ * @param {string} name
* The name or the full ID of the option element to look up.
*
* @param {string} [section_id]
* The ID of the UCI section containing the option to look up. May be
* omitted if a full ID is passed as first argument.
*
- * @param {string} [config]
- * The name of the UCI configuration the option instance is belonging to.
+ * @param {string} [config_name]
+ * The name of the UCI configuration the option instance belongs to.
* Defaults to the main UCI configuration of the map if omitted.
*
* @returns {Array<LuCI.form.AbstractValue,string>|null}
@@ -793,7 +797,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
*
* @param {string} [description]
* The description text of the form which is usually rendered as text
- * paragraph below the form title and before the actual form conents.
+ * paragraph below the form title and before the actual form contents.
* If omitted, the corresponding paragraph element will not be rendered.
*/
var CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
@@ -917,7 +921,7 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
* triggers input value reading and validation for each encountered child
* option element.
*
- * Options which are hidden due to unsatisified dependencies are skipped.
+ * Options which are hidden due to unsatisfied dependencies are skipped.
*
* @returns {Promise<void>}
* Returns a promise resolving once the values of all child elements have
@@ -963,7 +967,7 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
* contents. If omitted, no description will be rendered.
*
* @throws {Error}
- * Throws an exeption if a tab with the same `name` already exists.
+ * Throws an exception if a tab with the same `name` already exists.
*/
tab: function(name, title, description) {
if (this.tabs && this.tabs[name])
@@ -993,11 +997,11 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
* @param {LuCI.form.AbstractValue} optionclass
* The option class to use for rendering the configuration option. Note
* that this value must be the class itself, not a class instance obtained
- * from calling `new`. It must also be a class dervied from
+ * from calling `new`. It must also be a class derived from
* [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
*
* @param {...*} classargs
- * Additional arguments which are passed as-is to the contructor of the
+ * Additional arguments which are passed as-is to the constructor of the
* given option class. Refer to the class specific constructor
* documentation for details.
*
@@ -1020,17 +1024,17 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
/**
* Add a configuration option widget to a tab of the section.
*
- * @param {string} tabname
+ * @param {string} tabName
* The name of the section tab to add the option element to.
*
* @param {LuCI.form.AbstractValue} optionclass
* The option class to use for rendering the configuration option. Note
* that this value must be the class itself, not a class instance obtained
- * from calling `new`. It must also be a class dervied from
+ * from calling `new`. It must also be a class derived from
* [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
*
* @param {...*} classargs
- * Additional arguments which are passed as-is to the contructor of the
+ * Additional arguments which are passed as-is to the constructor of the
* given option class. Refer to the class specific constructor
* documentation for details.
*
@@ -1373,7 +1377,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
/**
* If set to `true`, the underlying ui input widget value is not cleared
- * from the configuration on unsatisfied depedencies. The default behavior
+ * from the configuration on unsatisfied dependencies. The default behavior
* is to remove the values of all options whose dependencies are not
* fulfilled.
*
@@ -1535,10 +1539,10 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
*/
/**
- * Add a dependency contraint to the option.
+ * Add a dependency constraint to the option.
*
* Dependency constraints allow making the presence of option elements
- * dependant on the current values of certain other options within the
+ * dependent on the current values of certain other options within the
* same form. An option element with unsatisfied dependencies will be
* hidden from the view and its current value is omitted when saving.
*
@@ -1550,7 +1554,7 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
* a logical "and" expression.
*
* Option names may be given in "dot notation" which allows to reference
- * option elements outside of the current form section. If a name without
+ * option elements outside the current form section. If a name without
* dot is specified, it refers to an option within the same configuration
* section. If specified as <code>configname.sectionid.optionname</code>,
* options anywhere within the same form may be specified.
@@ -1616,11 +1620,11 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
* </li>
* </ul>
*
- * @param {string|Object<string, string|RegExp>} optionname_or_depends
+ * @param {string|Object<string, string|RegExp>} field
* The name of the option to depend on or an object describing multiple
- * dependencies which must be satified (a logical "and" expression).
+ * dependencies which must be satisfied (a logical "and" expression).
*
- * @param {string} optionvalue|RegExp
+ * @param {string|RegExp} value
* 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.
@@ -2255,7 +2259,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
if (this.map.readonly !== true) {
ui.addValidator(nameEl, 'uciname', true, function(v) {
- var button = document.querySelector('.cbi-section-create > .cbi-button-add');
+ var button = createEl.querySelector('.cbi-section-create > .cbi-button-add');
if (v !== '') {
button.disabled = null;
return true;
@@ -2273,10 +2277,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
/** @private */
renderSectionPlaceholder: function() {
- return E([
- E('em', _('This section contains no values yet')),
- E('br'), E('br')
- ]);
+ return E('em', _('This section contains no values yet'));
},
/** @private */
@@ -2583,8 +2584,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (nodes.length == 0)
tableEl.appendChild(E('tr', { 'class': 'tr cbi-section-table-row placeholder' },
- E('td', { 'class': 'td' },
- E('em', {}, _('This section contains no values yet')))));
+ E('td', { 'class': 'td' }, this.renderSectionPlaceholder())));
sectionEl.appendChild(tableEl);
@@ -2615,7 +2615,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
if (has_titles) {
var trEl = E('tr', {
'class': 'tr cbi-section-table-titles ' + anon_class,
- 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
+ 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null,
+ 'click': this.sortable ? ui.createHandlerFn(this, 'handleSort') : null
});
for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
@@ -2624,7 +2625,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
trEl.appendChild(E('th', {
'class': 'th cbi-section-table-cell',
- 'data-widget': opt.__name__
+ 'data-widget': opt.__name__,
+ 'data-sortable-row': this.sortable ? '' : null
}));
if (opt.width != null)
@@ -2993,10 +2995,20 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
/** @private */
handleModalCancel: function(modalMap, ev) {
- var prevNode = this.getPreviousModalMap();
+ var prevNode = this.getPreviousModalMap(),
+ resetTasks = Promise.resolve();
if (prevNode) {
- var heading = prevNode.parentNode.querySelector('h4');
+ var heading = prevNode.parentNode.querySelector('h4'),
+ prevMap = dom.findClassInstance(prevNode);
+
+ while (prevMap) {
+ resetTasks = resetTasks
+ .then(L.bind(prevMap.load, prevMap))
+ .then(L.bind(prevMap.reset, prevMap));
+
+ prevMap = prevMap.parent;
+ }
prevNode.classList.add('flash');
prevNode.classList.remove('hidden');
@@ -3013,7 +3025,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
ui.hideModal();
}
- return Promise.resolve();
+ return resetTasks;
},
/** @private */
@@ -3030,10 +3042,68 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
return saveTasks
- .then(L.bind(this.handleModalCancel, this, modalMap, ev))
+ .then(L.bind(this.handleModalCancel, this, modalMap, ev, true))
.catch(function() {});
},
+ /** @private */
+ handleSort: function(ev) {
+ if (!ev.target.matches('th[data-sortable-row]'))
+ return;
+
+ var th = ev.target,
+ descending = (th.getAttribute('data-sort-direction') == 'desc'),
+ config_name = this.uciconfig || this.map.config,
+ index = 0,
+ list = [];
+
+ ev.currentTarget.querySelectorAll('th').forEach(function(other_th, i) {
+ if (other_th !== th)
+ other_th.removeAttribute('data-sort-direction');
+ else
+ index = i;
+ });
+
+ ev.currentTarget.parentNode.querySelectorAll('tr.cbi-section-table-row').forEach(L.bind(function(tr, i) {
+ var sid = tr.getAttribute('data-sid'),
+ opt = tr.childNodes[index].getAttribute('data-name'),
+ val = this.cfgvalue(sid, opt);
+
+ tr.querySelectorAll('.flash').forEach(function(n) {
+ n.classList.remove('flash')
+ });
+
+ list.push([
+ ui.Table.prototype.deriveSortKey((val != null) ? val.trim() : ''),
+ tr
+ ]);
+ }, this));
+
+ list.sort(function(a, b) {
+ return descending
+ ? -L.naturalCompare(a[0], b[0])
+ : L.naturalCompare(a[0], b[0]);
+ });
+
+ window.requestAnimationFrame(L.bind(function() {
+ var ref_sid, cur_sid;
+
+ for (var i = 0; i < list.length; i++) {
+ list[i][1].childNodes[index].classList.add('flash');
+ th.parentNode.parentNode.appendChild(list[i][1]);
+
+ cur_sid = list[i][1].getAttribute('data-sid');
+
+ if (ref_sid)
+ this.map.data.move(config_name, cur_sid, ref_sid, true);
+
+ ref_sid = cur_sid;
+ }
+
+ th.setAttribute('data-sort-direction', descending ? 'asc' : 'desc');
+ }, this));
+ },
+
/**
* Add further options to the per-section instanced modal popup.
*
@@ -3056,7 +3126,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
* @returns {*|Promise<*>}
* Return values of this function are ignored but if a promise is returned,
* it is run to completion before the rendering is continued, allowing
- * custom logic to perform asynchroneous work before the modal dialog
+ * custom logic to perform asynchronous work before the modal dialog
* is shown.
*/
addModalOptions: function(modalSection, section_id, ev) {
@@ -3077,33 +3147,38 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
},
/** @private */
- renderMoreOptionsModal: function(section_id, ev) {
- var parent = this.map,
- title = parent.title,
- name = null,
- m = new CBIMap(this.map.config, null, null),
- s = m.section(CBINamedSection, section_id, this.sectiontype);
+ cloneOptions: function(src_section, dest_section) {
+ for (var i = 0; i < src_section.children.length; i++) {
+ var o1 = src_section.children[i];
- m.parent = parent;
- m.readonly = parent.readonly;
+ if (o1.modalonly === false && src_section === this)
+ continue;
- s.tabs = this.tabs;
- s.tab_names = this.tab_names;
+ var o2;
- if ((name = this.titleFn('modaltitle', section_id)) != null)
- title = name;
- else if ((name = this.titleFn('sectiontitle', section_id)) != null)
- title = '%s - %s'.format(parent.title, name);
- else if (!this.anonymous)
- title = '%s - %s'.format(parent.title, section_id);
+ if (o1.subsection) {
+ o2 = dest_section.option(o1.constructor, o1.option, o1.subsection.constructor, o1.subsection.sectiontype, o1.subsection.title, o1.subsection.description);
- for (var i = 0; i < this.children.length; i++) {
- var o1 = this.children[i];
+ for (var k in o1.subsection) {
+ if (!o1.subsection.hasOwnProperty(k))
+ continue;
- if (o1.modalonly === false)
- continue;
+ switch (k) {
+ case 'map':
+ case 'children':
+ case 'parentoption':
+ continue;
- var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
+ default:
+ o2.subsection[k] = o1.subsection[k];
+ }
+ }
+
+ this.cloneOptions(o1.subsection, o2.subsection);
+ }
+ else {
+ o2 = dest_section.option(o1.constructor, o1.option, o1.title, o1.description);
+ }
for (var k in o1) {
if (!o1.hasOwnProperty(k))
@@ -3115,6 +3190,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
case 'option':
case 'title':
case 'description':
+ case 'subsection':
continue;
default:
@@ -3122,39 +3198,84 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}
}
}
+ },
- return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
- var modalMap = this.getActiveModalMap();
+ /** @private */
+ renderMoreOptionsModal: function(section_id, ev) {
+ var parent = this.map,
+ sref = parent.data.get(parent.config, section_id),
+ mapNode = this.getActiveModalMap(),
+ activeMap = mapNode ? dom.findClassInstance(mapNode) : null,
+ stackedMap = activeMap && (activeMap.parent !== parent || activeMap.section !== section_id);
- if (modalMap) {
- modalMap.parentNode
- .querySelector('h4')
- .appendChild(E('span', title ? ' » ' + title : ''));
+ return (stackedMap ? activeMap.save(null, true) : Promise.resolve()).then(L.bind(function() {
+ section_id = sref['.name'];
- modalMap.parentNode
- .querySelector('div.right > button')
- .firstChild.data = _('Back');
+ var m;
- modalMap.classList.add('hidden');
- modalMap.parentNode.insertBefore(nodes, modalMap.nextElementSibling);
- nodes.classList.add('flash');
+ if (parent instanceof CBIJSONMap) {
+ m = new CBIJSONMap(null, null, null);
+ m.data = parent.data;
}
else {
- ui.showModal(title, [
- nodes,
- E('div', { 'class': 'right' }, [
- E('button', {
- 'class': 'cbi-button',
- 'click': ui.createHandlerFn(this, 'handleModalCancel', m)
- }, [ _('Dismiss') ]), ' ',
- E('button', {
- 'class': 'cbi-button cbi-button-positive important',
- 'click': ui.createHandlerFn(this, 'handleModalSave', m),
- 'disabled': m.readonly || null
- }, [ _('Save') ])
- ])
- ], 'cbi-modal');
+ m = new CBIMap(parent.config, null, null);
}
+
+ var s = m.section(CBINamedSection, section_id, this.sectiontype);
+
+ m.parent = parent;
+ m.section = section_id;
+ m.readonly = parent.readonly;
+
+ s.tabs = this.tabs;
+ s.tab_names = this.tab_names;
+
+ this.cloneOptions(this, s);
+
+ return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(function() {
+ return m.render();
+ }).then(L.bind(function(nodes) {
+ var title = parent.title,
+ name = null;
+
+ if ((name = this.titleFn('modaltitle', section_id)) != null)
+ title = name;
+ else if ((name = this.titleFn('sectiontitle', section_id)) != null)
+ title = '%s - %s'.format(parent.title, name);
+ else if (!this.anonymous)
+ title = '%s - %s'.format(parent.title, section_id);
+
+ if (stackedMap) {
+ mapNode.parentNode
+ .querySelector('h4')
+ .appendChild(E('span', title ? ' » ' + title : ''));
+
+ mapNode.parentNode
+ .querySelector('div.right > button')
+ .firstChild.data = _('Back');
+
+ mapNode.classList.add('hidden');
+ mapNode.parentNode.insertBefore(nodes, mapNode.nextElementSibling);
+
+ nodes.classList.add('flash');
+ }
+ else {
+ ui.showModal(title, [
+ nodes,
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'cbi-button',
+ 'click': ui.createHandlerFn(this, 'handleModalCancel', m)
+ }, [ _('Dismiss') ]), ' ',
+ E('button', {
+ 'class': 'cbi-button cbi-button-positive important',
+ 'click': ui.createHandlerFn(this, 'handleModalSave', m),
+ 'disabled': m.readonly || null
+ }, [ _('Save') ])
+ ])
+ ], 'cbi-modal');
+ }
+ }, this));
}, this)).catch(L.error);
}
});
@@ -3177,7 +3298,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
*
* Another important difference is that the table cells show a readonly text
* preview of the corresponding option elements by default, unless the child
- * option element is explicitely made writable by setting the `editable`
+ * option element is explicitly made writable by setting the `editable`
* property to `true`.
*
* Additionally, the grid section honours a `modalonly` property of child
@@ -3226,7 +3347,7 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
* contents. If omitted, no description will be rendered.
*
* @throws {Error}
- * Throws an exeption if a tab with the same `name` already exists.
+ * Throws an exception if a tab with the same `name` already exists.
*/
tab: function(name, title, description) {
CBIAbstractSection.prototype.tab.call(this, name, title, description);
@@ -3249,20 +3370,19 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
var mapNode = this.getPreviousModalMap(),
prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
- return this.super('handleModalSave', arguments)
- .then(function() { delete prevMap.addedSection });
+ return this.super('handleModalSave', arguments);
},
/** @private */
- handleModalCancel: function(/* ... */) {
+ handleModalCancel: function(modalMap, ev, isSaving) {
var config_name = this.uciconfig || this.map.config,
mapNode = this.getPreviousModalMap(),
prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
- if (prevMap.addedSection != null) {
+ if (prevMap.addedSection != null && !isSaving)
this.map.data.remove(config_name, prevMap.addedSection);
- delete prevMap.addedSection;
- }
+
+ delete prevMap.addedSection;
return this.super('handleModalCancel', arguments);
},
@@ -3300,7 +3420,7 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
'data-title': (title != '') ? title : null,
'data-description': (descr != '') ? descr : null,
'data-name': opt.option,
- 'data-widget': opt.typename || opt.__name__
+ 'data-widget': 'CBI.DummyValue'
}, (value != null) ? value : E('em', _('none')));
},
@@ -3545,7 +3665,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
* @param {string} key
* The choice value to add.
*
- * @param {Node|string} value
+ * @param {Node|string} val
* The caption for the choice value. May be a DOM node, a document fragment
* or a plain text string. If omitted, the `key` value is used as caption.
*/
@@ -3642,7 +3762,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
if (!in_table && typeof(this.description) === 'string' && this.description !== '')
dom.append(optionEl.lastChild || optionEl,
- E('div', { 'class': 'cbi-value-description' }, this.description));
+ E('div', { 'class': 'cbi-value-description' }, this.description.trim()));
if (depend_list && depend_list.length)
optionEl.classList.add('hidden');
@@ -3757,7 +3877,7 @@ var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype
* @classdesc
*
* The `ListValue` class implements a simple static HTML select element
- * allowing the user to chose a single value from a set of predefined choices.
+ * allowing the user chose a single value from a set of predefined choices.
* It builds upon the {@link LuCI.ui.Select} widget.
*
* @param {LuCI.form.Map|LuCI.form.JSONMap} form
@@ -4206,7 +4326,7 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
__name__: 'CBI.DummyValue',
/**
- * Set an URL which is opened when clicking on the dummy value text.
+ * Set a URL which is opened when clicking on the dummy value text.
*
* By setting this property, the dummy value text is wrapped in an `<a>`
* element with the property value used as `href` attribute.
@@ -4679,7 +4799,7 @@ var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototyp
* @hideconstructor
* @classdesc
*
- * The LuCI form class provides high level abstractions for creating creating
+ * The LuCI form class provides high level abstractions for creating
* UCI- or JSON backed configurations forms.
*
* To import the class in views, use `'require form'`, to import it in
diff --git a/modules/luci-base/htdocs/luci-static/resources/fs.js b/modules/luci-base/htdocs/luci-static/resources/fs.js
index 99defb76c5..b64af04e56 100644
--- a/modules/luci-base/htdocs/luci-static/resources/fs.js
+++ b/modules/luci-base/htdocs/luci-static/resources/fs.js
@@ -229,7 +229,7 @@ var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
/**
* Unlink the given file.
*
- * @param {string}
+ * @param {string} path
* The file path to remove.
*
* @returns {Promise<number>}
@@ -345,7 +345,7 @@ var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
* @param {string} path
* The file path to read.
*
- * @param {string} [type=text]
+ * @param {"blob"|"text"|"blob"} [type=text]
* The expected type of read file contents. Valid values are `text` to
* interpret the contents as string, `json` to parse the contents as JSON
* or `blob` to return the contents as Blob instance.
@@ -387,7 +387,7 @@ var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
* @param {string[]} [params]
* The arguments to pass to the command.
*
- * @param {string} [type=text]
+ * @param {"blob"|"text"|"blob"} [type=text]
* The expected output type of the invoked program. Valid values are
* `text` to interpret the output as string, `json` to parse the output
* as JSON or `blob` to return the output as Blob instance.
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/alias.png b/modules/luci-base/htdocs/luci-static/resources/icons/alias.png
index a0c452c87a..94d556afa8 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/alias.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/alias.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/alias_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/alias_disabled.png
index 38d0531e36..48c41167df 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/alias_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/alias_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/bridge.png b/modules/luci-base/htdocs/luci-static/resources/icons/bridge.png
index 7faadecf92..beec3caf23 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/bridge.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/bridge.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/bridge_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/bridge_disabled.png
index b3e620b3a1..dce152a1dc 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/bridge_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/bridge_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/ethernet.png b/modules/luci-base/htdocs/luci-static/resources/icons/ethernet.png
index e3d24f2791..e7d37504aa 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/ethernet.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/ethernet.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/ethernet_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/ethernet_disabled.png
index d8792df54b..73086b3de7 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/ethernet_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/ethernet_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/port_down.png b/modules/luci-base/htdocs/luci-static/resources/icons/port_down.png
index 1ddf439f29..b3806146e9 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/port_down.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/port_down.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/port_up.png b/modules/luci-base/htdocs/luci-static/resources/icons/port_up.png
index fd801a4992..efb4fea585 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/port_up.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/port_up.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-0-25.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-0-25.png
index 382cf540bf..6455093c94 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-0-25.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-0-25.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-0.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-0.png
index c8192c8b9a..ed7d1cdfa9 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-0.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-0.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-25-50.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-25-50.png
index b465de3f57..43387ff666 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-25-50.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-25-50.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-50-75.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-50-75.png
index cd7bcaf9a6..48eeaa831b 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-50-75.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-50-75.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-75-100.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-75-100.png
index f7a3658df8..fd7a80da4c 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-75-100.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-75-100.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/signal-none.png b/modules/luci-base/htdocs/luci-static/resources/icons/signal-none.png
index 4a11356af2..944dd094be 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/signal-none.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/signal-none.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/switch.png b/modules/luci-base/htdocs/luci-static/resources/icons/switch.png
index 2691874a18..5c780fe66f 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/switch.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/switch.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/switch_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/switch_disabled.png
index 54588d24d1..a069afec4b 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/switch_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/switch_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/tunnel.png b/modules/luci-base/htdocs/luci-static/resources/icons/tunnel.png
index 63eabfef59..961467fe65 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/tunnel.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/tunnel.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/tunnel_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/tunnel_disabled.png
index ca79d81707..406b3508c1 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/tunnel_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/tunnel_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/vlan.png b/modules/luci-base/htdocs/luci-static/resources/icons/vlan.png
index 2691874a18..5c780fe66f 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/vlan.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/vlan.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/vlan_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/vlan_disabled.png
index 54588d24d1..a069afec4b 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/vlan_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/vlan_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/wifi.png b/modules/luci-base/htdocs/luci-static/resources/icons/wifi.png
index 80a23e8e9a..40da21f4f3 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/wifi.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/wifi.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/wifi_disabled.png b/modules/luci-base/htdocs/luci-static/resources/icons/wifi_disabled.png
index e989a2bd3d..4948024453 100644
--- a/modules/luci-base/htdocs/luci-static/resources/icons/wifi_disabled.png
+++ b/modules/luci-base/htdocs/luci-static/resources/icons/wifi_disabled.png
Binary files differ
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 78e8b8b30b..ca0e80a82a 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -81,7 +81,7 @@
* subclass.
*
* @returns {LuCI.baseclass}
- * Returns a new LuCI.baseclass sublassed from this class, extended
+ * Returns a new LuCI.baseclass subclassed from this class, extended
* by the given properties and with its prototype set to this base
* class to enable inheritance. The resulting value represents a
* class constructor and can be instantiated with `new`.
@@ -218,7 +218,7 @@
* would copy all values till the end.
*
* @param {...*} [extra_args]
- * Extra arguments to add to prepend to the resultung array.
+ * Extra arguments to add to prepend to the resulting array.
*
* @returns {Array<*>}
* Returns a new array consisting of the optional extra arguments
@@ -695,115 +695,117 @@
* The resulting HTTP response.
*/
request: function(target, options) {
- var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
- opt = Object.assign({}, options, state),
- content = null,
- contenttype = null,
- callback = this.handleReadyStateChange;
-
- return new Promise(function(resolveFn, rejectFn) {
- opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
- opt.method = String(opt.method || 'GET').toUpperCase();
-
- if ('query' in opt) {
- var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
- if (opt.query[k] != null) {
- var v = (typeof(opt.query[k]) == 'object')
- ? JSON.stringify(opt.query[k])
- : String(opt.query[k]);
-
- return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
- }
- else {
- return encodeURIComponent(k);
- }
- }).join('&') : '';
-
- if (q !== '') {
- switch (opt.method) {
- case 'GET':
- case 'HEAD':
- case 'OPTIONS':
- opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
- break;
-
- default:
- if (content == null) {
- content = q;
- contenttype = 'application/x-www-form-urlencoded';
+ return Promise.resolve(target).then((function(url) {
+ var state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() },
+ opt = Object.assign({}, options, state),
+ content = null,
+ contenttype = null,
+ callback = this.handleReadyStateChange;
+
+ return new Promise(function(resolveFn, rejectFn) {
+ opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
+ opt.method = String(opt.method || 'GET').toUpperCase();
+
+ if ('query' in opt) {
+ var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
+ if (opt.query[k] != null) {
+ var v = (typeof(opt.query[k]) == 'object')
+ ? JSON.stringify(opt.query[k])
+ : String(opt.query[k]);
+
+ return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
+ }
+ else {
+ return encodeURIComponent(k);
+ }
+ }).join('&') : '';
+
+ if (q !== '') {
+ switch (opt.method) {
+ case 'GET':
+ case 'HEAD':
+ case 'OPTIONS':
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
+ break;
+
+ default:
+ if (content == null) {
+ content = q;
+ contenttype = 'application/x-www-form-urlencoded';
+ }
}
}
}
- }
- if (!opt.cache)
- opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
+ if (!opt.cache)
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
- if (isQueueableRequest(opt)) {
- requestQueue.push([opt, rejectFn, resolveFn]);
- requestAnimationFrame(flushRequestQueue);
- return;
- }
+ if (isQueueableRequest(opt)) {
+ requestQueue.push([opt, rejectFn, resolveFn]);
+ requestAnimationFrame(flushRequestQueue);
+ return;
+ }
- if ('username' in opt && 'password' in opt)
- opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
- else
- opt.xhr.open(opt.method, opt.url, true);
+ if ('username' in opt && 'password' in opt)
+ opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
+ else
+ opt.xhr.open(opt.method, opt.url, true);
+
+ opt.xhr.responseType = opt.responseType || 'text';
- opt.xhr.responseType = opt.responseType || 'text';
+ if ('overrideMimeType' in opt.xhr)
+ opt.xhr.overrideMimeType('application/octet-stream');
- if ('overrideMimeType' in opt.xhr)
- opt.xhr.overrideMimeType('application/octet-stream');
+ if ('timeout' in opt)
+ opt.xhr.timeout = +opt.timeout;
- if ('timeout' in opt)
- opt.xhr.timeout = +opt.timeout;
+ if ('credentials' in opt)
+ opt.xhr.withCredentials = !!opt.credentials;
- if ('credentials' in opt)
- opt.xhr.withCredentials = !!opt.credentials;
+ if (opt.content != null) {
+ switch (typeof(opt.content)) {
+ case 'function':
+ content = opt.content(opt.xhr);
+ break;
- if (opt.content != null) {
- switch (typeof(opt.content)) {
- case 'function':
- content = opt.content(opt.xhr);
- break;
+ case 'object':
+ if (!(opt.content instanceof FormData)) {
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ }
+ else {
+ content = opt.content;
+ }
+ break;
- case 'object':
- if (!(opt.content instanceof FormData)) {
- content = JSON.stringify(opt.content);
- contenttype = 'application/json';
- }
- else {
- content = opt.content;
+ default:
+ content = String(opt.content);
}
- break;
-
- default:
- content = String(opt.content);
}
- }
- if ('headers' in opt)
- for (var header in opt.headers)
- if (opt.headers.hasOwnProperty(header)) {
- if (header.toLowerCase() != 'content-type')
- opt.xhr.setRequestHeader(header, opt.headers[header]);
- else
- contenttype = opt.headers[header];
- }
+ if ('headers' in opt)
+ for (var header in opt.headers)
+ if (opt.headers.hasOwnProperty(header)) {
+ if (header.toLowerCase() != 'content-type')
+ opt.xhr.setRequestHeader(header, opt.headers[header]);
+ else
+ contenttype = opt.headers[header];
+ }
- if ('progress' in opt && 'upload' in opt.xhr)
- opt.xhr.upload.addEventListener('progress', opt.progress);
+ if ('progress' in opt && 'upload' in opt.xhr)
+ opt.xhr.upload.addEventListener('progress', opt.progress);
- if (contenttype != null)
- opt.xhr.setRequestHeader('Content-Type', contenttype);
+ if (contenttype != null)
+ opt.xhr.setRequestHeader('Content-Type', contenttype);
- try {
- opt.xhr.send(content);
- }
- catch (e) {
- rejectFn.call(opt, e);
- }
- });
+ try {
+ opt.xhr.send(content);
+ }
+ catch (e) {
+ rejectFn.call(opt, e);
+ }
+ });
+ }).bind(this));
},
handleReadyStateChange: function(resolveFn, rejectFn, ev) {
@@ -834,7 +836,7 @@
*
* @instance
* @memberof LuCI.request
- * @param {string} target
+ * @param {string} url
* The URL to request.
*
* @param {LuCI.request.RequestOptions} [options]
@@ -852,7 +854,7 @@
*
* @instance
* @memberof LuCI.request
- * @param {string} target
+ * @param {string} url
* The URL to request.
*
* @param {*} [data]
@@ -923,7 +925,7 @@
* @hideconstructor
* @classdesc
*
- * The `Request.poll` class provides some convience wrappers around
+ * The `Request.poll` class provides some convince wrappers around
* {@link LuCI.poll} mainly to simplify registering repeating HTTP
* request calls as polling functions.
*/
@@ -1053,7 +1055,7 @@
/**
* Add a new operation to the polling loop. If the polling loop is not
- * already started at this point, it will be implicitely started.
+ * already started at this point, it will be implicitly started.
*
* @instance
* @memberof LuCI.poll
@@ -1096,8 +1098,8 @@
},
/**
- * Remove an operation from the polling loop. If no further operatons
- * are registered, the polling loop is implicitely stopped.
+ * Remove an operation from the polling loop. If no further operations
+ * are registered, the polling loop is implicitly stopped.
*
* @instance
* @memberof LuCI.poll
@@ -1159,7 +1161,7 @@
* @instance
* @memberof LuCI.poll
* @returns {boolean}
- * Returns `true` if polling has been stopped or `false` if it din't
+ * Returns `true` if polling has been stopped or `false` if it didn't
* run to begin with.
*/
stop: function() {
@@ -1329,7 +1331,7 @@
* The `Node` argument to append the children to.
*
* @param {*} [children]
- * The childrens to append to the given node.
+ * The children to append to the given node.
*
* When `children` is an array, then each item of the array
* will be either appended as child element or text node,
@@ -1344,11 +1346,11 @@
* as first and the return value of the `children` function as
* second parameter.
*
- * When `children` is is a DOM `Node` instance, it will be
+ * When `children` is a DOM `Node` instance, it will be
* appended to the given `node`.
*
* When `children` is any other non-`null` value, it will be
- * converted to a string and appened to the `innerHTML` property
+ * converted to a string and appended to the `innerHTML` property
* of the given `node`.
*
* @returns {Node|null}
@@ -1387,7 +1389,7 @@
* Replaces the content of the given node with the given children.
*
* This function first removes any children of the given DOM
- * `Node` and then adds the given given children following the
+ * `Node` and then adds the given children following the
* rules outlined below.
*
* @instance
@@ -1396,7 +1398,7 @@
* The `Node` argument to replace the children of.
*
* @param {*} [children]
- * The childrens to replace into the given node.
+ * The children to replace into the given node.
*
* When `children` is an array, then each item of the array
* will be either appended as child element or text node,
@@ -1411,11 +1413,11 @@
* as first and the return value of the `children` function as
* second parameter.
*
- * When `children` is is a DOM `Node` instance, it will be
+ * When `children` is a DOM `Node` instance, it will be
* appended to the given `node`.
*
* When `children` is any other non-`null` value, it will be
- * converted to a string and appened to the `innerHTML` property
+ * converted to a string and appended to the `innerHTML` property
* of the given `node`.
*
* @returns {Node|null}
@@ -1469,7 +1471,7 @@
*
* When `val` is of any other type, it will be added as attribute
* to the given `node` as-is, with the underlying `setAttribute()`
- * call implicitely turning it into a string.
+ * call implicitly turning it into a string.
*/
attr: function(node, key, val) {
if (!this.elem(node))
@@ -2032,7 +2034,7 @@
* Any return values of this function are discarded, but
* passed through `Promise.resolve()` to ensure that any
* returned promise runs to completion before the button
- * is reenabled.
+ * is re-enabled.
*/
handleSave: function(ev) {
var tasks = [];
@@ -2076,7 +2078,7 @@
* Any return values of this function are discarded, but
* passed through `Promise.resolve()` to ensure that any
* returned promise runs to completion before the button
- * is reenabled.
+ * is re-enabled.
*/
handleSaveApply: function(ev, mode) {
return this.handleSave(ev).then(function() {
@@ -2113,7 +2115,7 @@
* Any return values of this function are discarded, but
* passed through `Promise.resolve()` to ensure that any
* returned promise runs to completion before the button
- * is reenabled.
+ * is re-enabled.
*/
handleReset: function(ev) {
var tasks = [];
@@ -2181,7 +2183,7 @@
}).render() : E([]);
if (this.handleSaveApply || this.handleSave || this.handleReset) {
- footer.appendChild(E('div', { 'class': 'cbi-page-actions control-group' }, [
+ footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
saveApplyBtn, ' ',
this.handleSave ? E('button', {
'class': 'cbi-button cbi-button-save',
@@ -2218,6 +2220,8 @@
view: View
};
+ var naturalCompare = new Intl.Collator(undefined, { numeric: true }).compare;
+
var LuCI = Class.extend(/** @lends LuCI.prototype */ {
__name__: 'LuCI',
__init__: function(setenv) {
@@ -2323,7 +2327,7 @@
/**
* A wrapper around {@link LuCI#raise raise()} which also renders
- * the error either as modal overlay when `ui.js` is already loaed
+ * the error either as modal overlay when `ui.js` is already loaded
* or directly into the view body.
*
* @instance
@@ -2667,7 +2671,7 @@
var loc = window.location;
window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
}
- }, _('To login…')))
+ }, _('Log in…')))
]);
LuCI.prototype.raise('SessionError', 'Login session is expired');
@@ -2798,7 +2802,7 @@
* omitted, it defaults to an empty string.
*
* @param {string[]} [parts]
- * An array of parts to join into an URL path. Parts may contain
+ * An array of parts to join into a URL path. Parts may contain
* slashes and any of the other characters mentioned above.
*
* @return {string}
@@ -2818,7 +2822,7 @@
},
/**
- * Construct an URL pathrelative to the script path of the server
+ * Construct a URL with path relative to the script path of the server
* side LuCI application (usually `/cgi-bin/luci`).
*
* The resulting URL is guaranteed to only contain the characters
@@ -2829,7 +2833,7 @@
* @memberof LuCI
*
* @param {string[]} [parts]
- * An array of parts to join into an URL path. Parts may contain
+ * An array of parts to join into a URL path. Parts may contain
* slashes and any of the other characters mentioned above.
*
* @return {string}
@@ -2840,7 +2844,7 @@
},
/**
- * Construct an URL path relative to the global static resource path
+ * Construct a URL path relative to the global static resource path
* of the LuCI ui (usually `/luci-static/resources`).
*
* The resulting URL is guaranteed to only contain the characters
@@ -2851,7 +2855,7 @@
* @memberof LuCI
*
* @param {string[]} [parts]
- * An array of parts to join into an URL path. Parts may contain
+ * An array of parts to join into a URL path. Parts may contain
* slashes and any of the other characters mentioned above.
*
* @return {string}
@@ -2862,7 +2866,7 @@
},
/**
- * Construct an URL path relative to the media resource path of the
+ * Construct a URL path relative to the media resource path of the
* LuCI ui (usually `/luci-static/$theme_name`).
*
* The resulting URL is guaranteed to only contain the characters
@@ -2873,7 +2877,7 @@
* @memberof LuCI
*
* @param {string[]} [parts]
- * An array of parts to join into an URL path. Parts may contain
+ * An array of parts to join into a URL path. Parts may contain
* slashes and any of the other characters mentioned above.
*
* @return {string}
@@ -2927,14 +2931,14 @@
* The object to extract the keys from. If the given value is
* not an object, the function will return an empty array.
*
- * @param {string} [key]
+ * @param {string|null} [key]
* Specifies the key to order by. This is mainly useful for
* nested objects of objects or objects of arrays when sorting
* shall not be performed by the primary object keys but by
* some other key pointing to a value within the nested values.
*
- * @param {string} [sortmode]
- * May be either `addr` or `num` to override the natural
+ * @param {"addr"|"num"} [sortmode]
+ * Can be either `addr` or `num` to override the natural
* lexicographic sorting with a sorting suitable for IP/MAC style
* addresses or numeric values respectively.
*
@@ -2963,18 +2967,55 @@
}).filter(function(e) {
return (e[1] != null);
}).sort(function(a, b) {
- if (a[1] < b[1])
- return -1;
- else if (a[1] > b[1])
- return 1;
- else
- return 0;
+ return naturalCompare(a[1], b[1]);
}).map(function(e) {
return e[0];
});
},
/**
+ * Compares two values numerically and returns -1, 0 or 1 depending
+ * on whether the first value is smaller, equal to or larger than the
+ * second one respectively.
+ *
+ * This function is meant to be used as comparator function for
+ * Array.sort().
+ *
+ * @type {function}
+ *
+ * @param {*} a
+ * The first value
+ *
+ * @param {*} b
+ * The second value.
+ *
+ * @return {number}
+ * Returns -1 if the first value is smaller than the second one.
+ * Returns 0 if both values are equal.
+ * Returns 1 if the first value is larger than the second one.
+ */
+ naturalCompare: naturalCompare,
+
+ /**
+ * Converts the given value to an array using toArray() if needed,
+ * performs a numerical sort using naturalCompare() and returns the
+ * result. If the input already is an array, no copy is being made
+ * and the sorting is performed in-place.
+ *
+ * @see toArray
+ * @see naturalCompare
+ *
+ * @param {*} val
+ * The input value to sort (and convert to an array if needed).
+ *
+ * @return {Array<*>}
+ * Returns the resulting, numerically sorted array.
+ */
+ sortedArray: function(val) {
+ return this.toArray(val).sort(naturalCompare);
+ },
+
+ /**
* Converts the given value to an array. If the given value is of
* type array, it is returned as-is, values of type object are
* returned as one-element array containing the object, empty
@@ -3008,7 +3049,7 @@
},
/**
- * Returns a promise resolving with either the given value or or with
+ * Returns a promise resolving with either the given value or with
* the given default in case the input value is a rejecting promise.
*
* @instance
diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js
index 8c9ee255ff..fc58c3d758 100644
--- a/modules/luci-base/htdocs/luci-static/resources/network.js
+++ b/modules/luci-base/htdocs/luci-static/resources/network.js
@@ -101,15 +101,6 @@ 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 */
@@ -219,8 +210,8 @@ function getWifiNetidBySid(sid) {
var s = uci.get('wireless', sid);
if (s != null && s['.type'] == 'wifi-iface') {
var radioname = s.device;
- if (typeof(s.device) == 'string') {
- var i = 0, netid = null, sections = uci.sections('wireless', 'wifi-iface');
+ if (typeof(radioname) == 'string') {
+ var sections = uci.sections('wireless', 'wifi-iface');
for (var i = 0, n = 0; i < sections.length; i++) {
if (sections[i].device != s.device)
continue;
@@ -485,10 +476,7 @@ function initNetworkState(refresh) {
}
ports.sort(function(a, b) {
- if (a.role != b.role)
- return (a.role < b.role) ? -1 : 1;
-
- return (a.index - b.index);
+ return L.naturalCompare(a.role, b.role) || L.naturalCompare(a.index, b.index);
});
for (var i = 0, port; (port = ports[i]) != null; i++) {
@@ -562,18 +550,14 @@ function ifnameOf(obj) {
}
function networkSort(a, b) {
- return strcmp(a.getName(), b.getName());
+ return L.naturalCompare(a.getName(), b.getName());
}
function deviceSort(a, b) {
- var typeWeigth = { wifi: 2, alias: 3 },
- weightA = typeWeigth[a.getType()] || 1,
- weightB = typeWeigth[b.getType()] || 1;
+ var typeWeigth = { wifi: 2, alias: 3 };
- if (weightA != weightB)
- return weightA - weightB;
-
- return strcmp(a.getName(), b.getName());
+ return L.naturalCompare(typeWeigth[a.getType()] || 1, typeWeigth[b.getType()] || 1) ||
+ L.naturalCompare(a.getName(), b.getName());
}
function formatWifiEncryption(enc) {
@@ -695,7 +679,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
* @method
*
* @param {string} netmask
- * The netmask to convert into a bit count.
+ * The netmask to convert into a bits count.
*
* @param {boolean} [v6=false]
* Whether to parse the given netmask as IPv4 (`false`) or IPv6 (`true`)
@@ -1441,7 +1425,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
rv.push(this.lookupWifiNetwork(wifiIfaces[i]['.name']));
rv.sort(function(a, b) {
- return strcmp(a.getID(), b.getID());
+ return L.naturalCompare(a.getID(), b.getID());
});
return rv;
@@ -1539,10 +1523,7 @@ 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 L.naturalCompare(a.metric, b.metric) || L.naturalCompare(a.interface, b.interface);
});
return rv;
@@ -1630,7 +1611,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
},
/**
- * Describes an swconfig switch topology by specifying the CPU
+ * Describes a swconfig switch topology by specifying the CPU
* connections and external port labels of a switch.
*
* @typedef {Object<string, Object|Array>} SwitchTopology
@@ -1645,7 +1626,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
* @property {Array<Object<string, boolean|number|string>>} ports
* The `ports` property points to an array describing the populated
* ports of the switch in the external label order. Each array item is
- * an object containg the following keys:
+ * an object containing the following keys:
* - `num` - the internal switch port number
* - `label` - the label of the port, e.g. `LAN 1` or `CPU (eth0)`
* - `device` - the connected Linux network device name (CPU ports only)
@@ -1749,7 +1730,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
},
/**
- * Obtains the the network device name of the given object.
+ * Obtains the network device name of the given object.
*
* @param {LuCI.network.Protocol|LuCI.network.Device|LuCI.network.WifiDevice|LuCI.network.WifiNetwork|string} obj
* The object to get the device name from.
@@ -1990,7 +1971,7 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ {
}
return rv.sort(function(a, b) {
- return strcmp(a[0], b[0]);
+ return L.naturalCompare(a[0], b[0]);
});
}
});
@@ -2057,7 +2038,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
},
/**
- * Get the associared Linux network device of this network.
+ * Get the associated Linux network device of this network.
*
* @returns {null|string}
* Returns the name of the associated network device or `null` if
@@ -2094,7 +2075,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
},
/**
- * Return a human readable description for the protcol, such as
+ * Return a human readable description for the protocol, such as
* `Static address` or `DHCP client`.
*
* This function should be overwritten by subclasses.
@@ -2445,7 +2426,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
*
* @returns {string}
* Returns the name of the opkg package required for the protocol to
- * function, e.g. `odhcp6c` for the `dhcpv6` prototocol.
+ * function, e.g. `odhcp6c` for the `dhcpv6` protocol.
*/
getOpkgPackage: function() {
return null;
@@ -2492,7 +2473,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* on demand instead of using existing physical interfaces.
*
* Examples for virtual protocols are `6in4` which `gre` spawn tunnel
- * network device on startup, examples for non-virtual protcols are
+ * network device on startup, examples for non-virtual protocols are
* `dhcp` or `static` which apply IP configuration to existing interfaces.
*
* This function should be overwritten by subclasses.
@@ -2509,7 +2490,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* Checks whether this protocol is "floating".
*
* A "floating" protocol is a protocol which spawns its own interfaces
- * on demand, like a virtual one but which relies on an existinf lower
+ * on demand, like a virtual one but which relies on an existing lower
* level interface to initiate the connection.
*
* An example for such a protocol is "pppoe".
@@ -2722,7 +2703,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
* interface.
*
* @returns {null|Array<LuCI.network.Device>}
- * Returns an array of of `Network.Device` class instances representing
+ * Returns an array of `Network.Device` class instances representing
* the sub-devices attached to this logical interface or `null` if the
* logical interface does not support sub-devices, e.g. because it is
* virtual and not a bridge.
@@ -2852,6 +2833,15 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
this.device = this.device || device;
this.dev = Object.assign({}, _state.netdevs[this.device]);
this.network = network;
+
+ var conf;
+
+ uci.sections('network', 'device', function(s) {
+ if (s.name == device)
+ conf = s;
+ });
+
+ this.config = Object.assign({}, conf);
},
_devstate: function(/* ... */) {
@@ -2946,6 +2936,10 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
return 'vlan';
else if (this.dev.devtype == 'dsa' || _state.isSwitch[this.device])
return 'switch';
+ else if (this.config.type == '8021q' || this.config.type == '8021ad')
+ return 'vlan';
+ else if (this.config.type == 'bridge')
+ return 'bridge';
else
return 'ethernet';
},
@@ -3245,7 +3239,13 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
* ordinary ethernet interfaces.
*/
getParent: function() {
- return this.dev.parent ? Network.prototype.instantiateDevice(this.dev.parent) : null;
+ if (this.dev.parent)
+ return Network.prototype.instantiateDevice(this.dev.parent);
+
+ if ((this.config.type == '8021q' || this.config.type == '802ad') && typeof(this.config.ifname) == 'string')
+ return Network.prototype.instantiateDevice(this.config.ifname);
+
+ return null;
}
});
@@ -3309,7 +3309,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
* @param {string} opt
* The name of the UCI option to set.
*
- * @param {null|string|string[]} val
+ * @param {null|string|string[]} value
* The value to set or `null` to remove the given option from the
* configuration.
*/
@@ -3396,28 +3396,20 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
getI18n: function() {
var hw = this.ubus('dev', 'iwinfo', 'hardware'),
type = L.isObject(hw) ? hw.name : null;
+ var modes = this.ubus('dev', 'iwinfo', 'hwmodes_text');
if (this.ubus('dev', 'iwinfo', 'type') == 'wl')
type = 'Broadcom';
- var hwmodes = this.getHWModes(),
- modestr = '';
-
- hwmodes.sort(function(a, b) {
- if (a.length != b.length)
- return a.length - b.length;
-
- return strcmp(a, b);
- });
-
- modestr = hwmodes.join('');
-
- return '%s 802.11%s Wireless Controller (%s)'.format(type || 'Generic', modestr, this.getName());
+ return '%s %s Wireless Controller (%s)'.format(
+ type || 'Generic',
+ modes ? '802.11' + modes : 'unknown',
+ this.getName());
},
/**
* A wireless scan result object describes a neighbouring wireless
- * network found in the vincinity.
+ * network found in the vicinity.
*
* @typedef {Object<string, number|string|LuCI.network.WifiEncryption>} WifiScanResult
* @memberof LuCI.network
@@ -3455,7 +3447,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
*
* @returns {Promise<Array<LuCI.network.WifiScanResult>>}
* Returns a promise resolving to an array of scan result objects
- * describing the networks found in the vincinity.
+ * describing the networks found in the vicinity.
*/
getScanList: function() {
return callIwinfoScan(this.sid);
@@ -3506,7 +3498,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
*
* @returns {Promise<Array<LuCI.network.WifiNetwork>>}
* Returns a promise resolving to an array of `Network.WifiNetwork`
- * instances respresenting the wireless networks associated with this
+ * instances representing the wireless networks associated with this
* radio device.
*/
getWifiNetworks: function() {
@@ -3635,7 +3627,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* @param {string} opt
* The name of the UCI option to set.
*
- * @param {null|string|string[]} val
+ * @param {null|string|string[]} value
* The value to set or `null` to remove the given option from the
* configuration.
*/
@@ -3722,7 +3714,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
/**
* Get the internal network ID of this wireless network.
*
- * The network ID is a LuCI specific identifer in the form
+ * The network ID is a LuCI specific identifier in the form
* `radio#.network#` to identify wireless networks by their corresponding
* radio and network index numbers.
*
@@ -3904,7 +3896,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* Query the current encryption settings from runtime information.
*
* @returns {string}
- * Returns a string describing the current encryption or `-` if the the
+ * Returns a string describing the current encryption or `-` if the
* encryption state could not be found in `ubus` runtime information.
*/
getActiveEncryption: function() {
@@ -4005,7 +3997,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* - `UNKNOWN`
*
* @property {number} [mesh non-peer PS]
- * The powersafe mode for all non-peer neigbours, may be an empty
+ * The powersafe mode for all non-peer neighbours, may be an empty
* string (`''`) or absent if not applicable or supported by the driver.
*
* The following modes are known:
@@ -4040,7 +4032,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* The amount of bytes that have been received or sent.
*
* @property {number} [failed]
- * The amount of failed tranmission attempts. Only applicable to
+ * The amount of failed transmission attempts. Only applicable to
* transmit rates.
*
* @property {number} [retries]
@@ -4064,7 +4056,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* HT or VHT rates.
*
* @property {number} [40mhz]
- * Specifies whether the tranmission rate used 40MHz wide channel.
+ * Specifies whether the transmission rate used 40MHz wide channel.
* Only applicable to HT or VHT rates.
*
* Note: this option exists for backwards compatibility only and its
@@ -4329,18 +4321,18 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
*
* @returns {LuCI.network.Device}
* Returns a `Network.Device` instance representing the Linux network
- * device associted with this wireless network.
+ * device associated with this wireless network.
*/
getDevice: function() {
return Network.prototype.instantiateDevice(this.getIfname());
},
/**
- * Check whether this wifi network supports deauthenticating clients.
+ * Check whether this wifi network supports de-authenticating clients.
*
* @returns {boolean}
* Returns `true` when this wifi network instance supports forcibly
- * deauthenticating clients, otherwise `false`.
+ * de-authenticating clients, otherwise `false`.
*/
isClientDisconnectSupported: function() {
return L.isObject(this.ubus('hostapd', 'del_client'));
@@ -4353,7 +4345,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* The MAC address of the client to disconnect.
*
* @param {boolean} [deauth=false]
- * Specifies whether to deauthenticate (`true`) or disassociate (`false`)
+ * Specifies whether to de-authenticate (`true`) or disassociate (`false`)
* the client.
*
* @param {number} [reason=1]
@@ -4363,7 +4355,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* @param {number} [ban_time=0]
* Specifies the amount of milliseconds to ban the client from
* reconnecting. By default, no ban time is set which allows the client
- * to reassociate / reauthenticate immediately.
+ * to re-associate / reauthenticate immediately.
*
* @returns {Promise<number>}
* Returns a promise resolving to the underlying ubus call result code
diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js
index f37f7bb6a4..8010066594 100644
--- a/modules/luci-base/htdocs/luci-static/resources/rpc.js
+++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js
@@ -186,7 +186,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
* the corresponding args object sent to the remote procedure will be
* `{ foo: true, bar: false }`.
* - `params: [ "test" ], filter: function(reply, args, extra) { ... }` -
- * When the resultung generated function is invoked with
+ * When the resulting generated function is invoked with
* `fn("foo", "bar", "baz")` then `{ "test": "foo" }` will be sent as
* argument to the remote procedure and the filter function will be
* invoked with `filterFn(reply, [ "foo" ], "bar", "baz")`
@@ -226,7 +226,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
* be returned as default instead.
*
* @property {LuCI.rpc~filterFn} [filter]
- * Specfies an optional filter function which is invoked to transform the
+ * Specifies an optional filter function which is invoked to transform the
* received reply data before it is returned to the caller.
*
* @property {boolean} [reject=false]
@@ -379,7 +379,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
/**
* Set the RPC base URL to use.
*
- * @param {string} sid
+ * @param {string} url
* Sets the RPC URL endpoint to issue requests against.
*/
setBaseURL: function(url) {
@@ -454,7 +454,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
* Registers a new interceptor function.
*
* @param {LuCI.rpc~interceptorFn} interceptorFn
- * The inteceptor function to register.
+ * The interceptor function to register.
*
* @returns {LuCI.rpc~interceptorFn}
* Returns the given function value.
@@ -469,7 +469,7 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
* Removes a registered interceptor function.
*
* @param {LuCI.rpc~interceptorFn} interceptorFn
- * The inteceptor function to remove.
+ * The interceptor function to remove.
*
* @returns {boolean}
* Returns `true` if the given function has been removed or `false`
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 10b65be8ec..14948bbf0f 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -187,9 +187,10 @@ var CBIZoneSelect = form.ListValue.extend({
emptyval.setAttribute('data-value', '');
}
- L.dom.content(emptyval.querySelector('span'), [
- E('strong', _('Device')), E('span', ' (%s)'.format(_('input')))
- ]);
+ if (opt[0].allowlocal)
+ L.dom.content(emptyval.querySelector('span'), [
+ E('strong', _('Device')), E('span', ' (%s)'.format(_('input')))
+ ]);
L.dom.content(anyval.querySelector('span'), [
E('strong', _('Any zone')), E('span', ' (%s)'.format(_('forward')))
@@ -536,7 +537,7 @@ var CBIDeviceSelect = form.ListValue.extend({
}
if (!this.nocreate) {
- var keys = Object.keys(checked).sort();
+ var keys = Object.keys(checked).sort(L.naturalCompare);
for (var i = 0; i < keys.length; i++) {
if (choices.hasOwnProperty(keys[i]))
diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js
index 41e902c5fe..76b274470b 100644
--- a/modules/luci-base/htdocs/luci-static/resources/uci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/uci.js
@@ -2,6 +2,14 @@
'require rpc';
'require baseclass';
+function isEmpty(object, ignore) {
+ for (var property in object)
+ if (object.hasOwnProperty(property) && property != ignore)
+ return false;
+
+ return true;
+}
+
/**
* @class uci
* @memberof LuCI
@@ -10,7 +18,7 @@
*
* The `LuCI.uci` class utilizes {@link LuCI.rpc} to declare low level
* remote UCI `ubus` procedures and implements a local caching and data
- * manipulation layer on top to allow for synchroneous operations on
+ * manipulation layer on top to allow for synchronous operations on
* UCI configuration data.
*/
return baseclass.extend(/** @lends LuCI.uci.prototype */ {
@@ -85,7 +93,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* identifier in the form `cfgXXXXXX` once the configuration is saved
* by the remote `ubus` UCI api.
*
- * @param {string} config
+ * @param {string} conf
* The configuration to generate the new section ID for.
*
* @returns {string}
@@ -108,7 +116,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* Resolves a given section ID in extended notation to the internal
* section ID value.
*
- * @param {string} config
+ * @param {string} conf
* The configuration to resolve the section ID for.
*
* @param {string} sid
@@ -201,7 +209,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* To force reloading a configuration, it has to be unloaded with
* {@link LuCI.uci#unload uci.unload()} first.
*
- * @param {string|string[]} config
+ * @param {string|string[]} packages
* The name of the configuration or an array of configuration
* names to load.
*
@@ -237,7 +245,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/**
* Unloads the given UCI configurations from the local cache.
*
- * @param {string|string[]} config
+ * @param {string|string[]} packages
* The name of the configuration or an array of configuration
* names to unload.
*/
@@ -259,7 +267,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* Adds a new section of the given type to the given configuration,
* optionally named according to the given name.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to add the section to.
*
* @param {string} type
@@ -294,7 +302,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/**
* Removes the section with the given ID from the given configuration.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to remove the section from.
*
* @param {string} sid
@@ -326,7 +334,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* enclosed within a configuration section, as well as some additional
* meta data such as sort indexes and internal ID.
*
- * Any internal metadata fields are prefixed with a dot which is isn't
+ * Any internal metadata fields are prefixed with a dot which isn't
* an allowed character for normal option names.
*
* @typedef {Object<string, boolean|number|string|string[]>} SectionObject
@@ -337,7 +345,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* anonymous (`true`) or named (`false`).
*
* @property {number} .index
- * The `.index` property specifes the sort order of the section.
+ * The `.index` property specifies the sort order of the section.
*
* @property {string} .name
* The `.name` property holds the name of the section object. It may be
@@ -375,7 +383,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* Enumerates the sections of the given configuration, optionally
* filtered by type.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to enumerate the sections for.
*
* @param {string} [type]
@@ -428,13 +436,13 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* of the given configuration or the entire section object if the
* option name is omitted.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to read the value from.
*
* @param {string} sid
* The name or ID of the section to read.
*
- * @param {string} [option]
+ * @param {string} [opt]
* The option name to read the value from. If the option name is
* omitted or `null`, the entire section is returned instead.
*
@@ -522,16 +530,16 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* If either config, section or option is null, or if `option` begins
* with a dot, the function will do nothing.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to set the option value in.
*
* @param {string} sid
* The name or ID of the section to set the option value in.
*
- * @param {string} option
+ * @param {string} opt
* The option name to set the value for.
*
- * @param {null|string|string[]} value
+ * @param {null|string|string[]} val
* The option value to set. If the value is `null` or an empty string,
* the option will be removed, otherwise it will be set or overwritten
* with the given value.
@@ -570,16 +578,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/* undelete option */
if (d[conf] && d[conf][sid]) {
- var empty = true;
-
- for (var key in d[conf][sid]) {
- if (key != opt && d[conf][sid].hasOwnProperty(key)) {
- empty = false;
- break;
- }
- }
-
- if (empty)
+ if (isEmpty(d[conf][sid], opt))
delete d[conf][sid];
else
delete d[conf][sid][opt];
@@ -589,8 +588,12 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
}
else {
/* revert any change for to-be-deleted option */
- if (c[conf] && c[conf][sid])
- delete c[conf][sid][opt];
+ if (c[conf] && c[conf][sid]) {
+ if (isEmpty(c[conf][sid], opt))
+ delete c[conf][sid];
+ else
+ delete c[conf][sid][opt];
+ }
/* only delete existing options */
if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) {
@@ -613,13 +616,13 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* This function is a convenience wrapper around
* `uci.set(config, section, option, null)`.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to remove the option from.
*
* @param {string} sid
* The name or ID of the section to remove the option from.
*
- * @param {string} option
+ * @param {string} opt
* The name of the option to remove.
*/
unset: function(conf, sid, opt) {
@@ -629,9 +632,9 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
/**
* Gets the value of the given option or the entire section object of
* the first found section of the specified type or the first found
- * section of the entire configuration if no type is specfied.
+ * section of the entire configuration if no type is specified.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to read the value from.
*
* @param {string} [type]
@@ -639,7 +642,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* section of the entire config is read, otherwise the first section
* matching the given type.
*
- * @param {string} [option]
+ * @param {string} [opt]
* The option name to read the value from. If the option name is
* omitted or `null`, the entire section is returned instead.
*
@@ -672,7 +675,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* If either config, type or option is null, or if `option` begins
* with a dot, the function will do nothing.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to set the option value in.
*
* @param {string} [type]
@@ -680,10 +683,10 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* section of the entire config is written to, otherwise the first
* section matching the given type is used.
*
- * @param {string} option
+ * @param {string} opt
* The option name to set the value for.
*
- * @param {null|string|string[]} value
+ * @param {null|string|string[]} val
* The option value to set. If the value is `null` or an empty string,
* the option will be removed, otherwise it will be set or overwritten
* with the given value.
@@ -707,7 +710,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* This function is a convenience wrapper around
* `uci.set_first(config, type, option, null)`.
*
- * @param {string} config
+ * @param {string} conf
* The name of the configuration to set the option value in.
*
* @param {string} [type]
@@ -715,7 +718,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* section of the entire config is written to, otherwise the first
* section matching the given type is used.
*
- * @param {string} option
+ * @param {string} opt
* The option name to set the value for.
*/
unset_first: function(conf, type, opt) {
@@ -726,7 +729,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* Move the first specified section within the given configuration
* before or after the second specified section.
*
- * @param {string} config
+ * @param {string} conf
* The configuration to move the section within.
*
* @param {string} sid1
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index ac158f5260..5502dfed25 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -26,7 +26,7 @@ var modalDiv = null,
* events.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -134,7 +134,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
* @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
+ * `false` if it is unchanged. Note that if the user modifies the initial
* value and changes it back to the original state, it is still reported
* as changed.
*/
@@ -316,7 +316,7 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
* The `Textfield` class implements a standard single line text input field.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -374,6 +374,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
'disabled': this.options.disabled ? '' : null,
'maxlength': this.options.maxlength,
'placeholder': this.options.placeholder,
+ 'autocomplete': this.options.password ? 'new-password' : null,
'value': this.value,
});
@@ -440,7 +441,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
* The `Textarea` class implements a multiline text area input field.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -556,7 +557,7 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
* The `Checkbox` class implements a simple checkbox input field.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -695,7 +696,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
* values are enabled or not.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -725,7 +726,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
* @property {boolean} [multiple=false]
* Specifies whether multiple choice values may be selected.
*
- * @property {string} [widget=select]
+ * @property {"select"|"individual"} [widget=select]
* Specifies the kind of widget to render. May be either `select` or
* `individual`. When set to `select` an HTML `<select>` element will be
* used, otherwise a group of checkbox or radio button elements is created,
@@ -777,7 +778,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
keys = Object.keys(this.choices);
if (this.options.sort === true)
- keys.sort();
+ keys.sort(L.naturalCompare);
else if (Array.isArray(this.options.sort))
keys = this.options.sort;
@@ -903,7 +904,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
* supports non-text choice labels.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -1050,13 +1051,14 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
'class': 'cbi-dropdown',
'multiple': this.options.multiple ? '' : null,
'optional': this.options.optional ? '' : null,
- 'disabled': this.options.disabled ? '' : null
+ 'disabled': this.options.disabled ? '' : null,
+ 'tabindex': -1
}, E('ul'));
var keys = Object.keys(this.choices);
if (this.options.sort === true)
- keys.sort();
+ keys.sort(L.naturalCompare);
else if (Array.isArray(this.options.sort))
keys = this.options.sort;
@@ -1186,11 +1188,11 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
}
else {
sb.addEventListener('mouseover', this.handleMouseover.bind(this));
+ sb.addEventListener('mouseout', this.handleMouseout.bind(this));
sb.addEventListener('focus', this.handleFocus.bind(this));
canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
- window.addEventListener('mouseover', this.setFocus);
window.addEventListener('click', this.closeAllDropdowns);
}
@@ -1343,7 +1345,12 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
sb.lastElementChild.setAttribute('tabindex', 0);
- this.setFocus(sb, sel || li[0], true);
+ var focusFn = L.bind(function(el) {
+ this.setFocus(sb, el, true);
+ ul.removeEventListener('transitionend', focusFn);
+ }, this, sel || li[0]);
+
+ ul.addEventListener('transitionend', focusFn);
},
/** @private */
@@ -1559,26 +1566,33 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
/** @private */
setFocus: function(sb, elem, scroll) {
- if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
+ if (sb.hasAttribute('locked-in'))
return;
- if (sb.target && findParent(sb.target, 'ul.dropdown'))
+ sb.querySelectorAll('.focus').forEach(function(e) {
+ e.classList.remove('focus');
+ });
+
+ elem.classList.add('focus');
+
+ if (scroll)
+ elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
+
+ elem.focus();
+ },
+
+ /** @private */
+ handleMouseout: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open'))
return;
- document.querySelectorAll('.focus').forEach(function(e) {
- if (!matchesElem(e, 'input')) {
- e.classList.remove('focus');
- e.blur();
- }
+ sb.querySelectorAll('.focus').forEach(function(e) {
+ e.classList.remove('focus');
});
- if (elem) {
- elem.focus();
- elem.classList.add('focus');
-
- if (scroll)
- elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
- }
+ sb.querySelector('ul.dropdown').focus();
},
/** @private */
@@ -1758,7 +1772,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
/** @private */
handleKeydown: function(ev) {
- var sb = ev.currentTarget;
+ var sb = ev.currentTarget,
+ ul = sb.querySelector('ul.dropdown');
if (matchesElem(ev.target, 'input'))
return;
@@ -1779,6 +1794,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
switch (ev.keyCode) {
case 27:
this.closeDropdown(sb);
+ ev.stopPropagation();
break;
case 13:
@@ -1802,6 +1818,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
this.setFocus(sb, active.previousElementSibling);
ev.preventDefault();
}
+ else if (document.activeElement === ul) {
+ this.setFocus(sb, ul.lastElementChild);
+ ev.preventDefault();
+ }
break;
case 40:
@@ -1809,6 +1829,10 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
this.setFocus(sb, active.nextElementSibling);
ev.preventDefault();
}
+ else if (document.activeElement === ul) {
+ this.setFocus(sb, ul.firstElementChild);
+ ev.preventDefault();
+ }
break;
}
}
@@ -1964,7 +1988,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
* with a set of enforced default properties for easier instantiation.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -2031,7 +2055,7 @@ var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
* into a dropdown to chose from a set of different action choices.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -2054,7 +2078,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
/**
* ComboButtons support the same properties as
* [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
- * specific values for some properties and add aditional button specific
+ * specific values for some properties and add additional button specific
* properties.
*
* @typedef {LuCI.ui.Dropdown.InitOptions} InitOptions
@@ -2148,7 +2172,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
* from a set of predefined choices.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -2171,7 +2195,7 @@ var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype *
*/
var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
/**
- * In case choices are passed to the dynamic list contructor, the widget
+ * In case choices are passed to the dynamic list constructor, the widget
* supports the same properties as [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions}
* but enforces specific values for some dropdown properties.
*
@@ -2208,7 +2232,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
'id': this.options.id,
'class': 'cbi-dynlist',
'disabled': this.options.disabled ? '' : null
- }, E('div', { 'class': 'add-item' }));
+ }, E('div', { 'class': 'add-item control-group' }));
if (this.choices) {
if (this.options.placeholder != null)
@@ -2503,7 +2527,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
* which allows to store form data without exposing it to the user.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -2569,7 +2593,7 @@ var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */
* browse, select and delete files beneath a predefined remote directory.
*
* UI widget instances are usually not supposed to be created by view code
- * directly, instead they're implicitely created by `LuCI.form` when
+ * directly, instead they're implicitly created by `LuCI.form` when
* instantiating CBI forms.
*
* This class is automatically instantiated as part of `LuCI.ui`. To use it
@@ -2614,9 +2638,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
*
* @property {string} [root_directory=/etc/luci-uploads]
* Specifies the remote directory the upload and file browsing actions take
- * place in. Browsing to directories outside of the root directory is
+ * place in. Browsing to directories outside the root directory is
* prevented by the widget. Note that this is not a security feature.
- * Whether remote directories are browseable or not solely depends on the
+ * Whether remote directories are browsable or not solely depends on the
* ACL setup for the current session.
*/
__init__: function(value, options) {
@@ -2859,13 +2883,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
rows = E('ul');
list.sort(function(a, b) {
- var isDirA = (a.type == 'directory'),
- isDirB = (b.type == 'directory');
-
- if (isDirA != isDirB)
- return isDirA < isDirB;
-
- return a.name > b.name;
+ return L.naturalCompare(a.type == 'directory', b.type == 'directory') ||
+ L.naturalCompare(a.name, b.name);
});
for (var i = 0; i < list.length; i++) {
@@ -3066,7 +3085,7 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
* @property {string} name - The internal name of the node, as used in the URL
* @property {number} order - The sort index of the menu node
* @property {string} [title] - The title of the menu node, `null` if the node should be hidden
- * @property {satisified} boolean - Boolean indicating whether the menu enries dependencies are satisfied
+ * @property {satisfied} boolean - Boolean indicating whether the menu entries dependencies are satisfied
* @property {readonly} [boolean] - Boolean indicating whether the menu entries underlying ACLs are readonly
* @property {LuCI.ui.menu.MenuNode[]} [children] - Array of child menu nodes.
*/
@@ -3125,7 +3144,24 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
if (!node.children[k].hasOwnProperty('title'))
continue;
- children.push(Object.assign(node.children[k], { name: k }));
+ var subnode = Object.assign(node.children[k], { name: k });
+
+ if (L.isObject(subnode.action) && subnode.action.path != null &&
+ (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
+ var root = this.menu,
+ path = subnode.action.path.split('/');
+
+ for (var i = 0; root != null && i < path.length; i++)
+ root = L.isObject(root.children) ? root.children[path[i]] : null;
+
+ if (root)
+ subnode = Object.assign({}, subnode, {
+ children: root.children,
+ action: root.action
+ });
+ }
+
+ children.push(subnode);
}
return children.sort(function(a, b) {
@@ -3135,11 +3171,316 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
if (wA != wB)
return wA - wB;
- return a.name > b.name;
+ return L.naturalCompare(a.name, b.name);
});
}
});
+var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
+ __init__: function(captions, options, placeholder) {
+ if (!Array.isArray(captions)) {
+ this.initFromMarkup(captions);
+
+ return;
+ }
+
+ var id = options.id || 'table%08x'.format(Math.random() * 0xffffffff);
+
+ var table = E('table', { 'id': id, 'class': 'table' }, [
+ E('tr', { 'class': 'tr table-titles', 'click': UI.prototype.createHandlerFn(this, 'handleSort') })
+ ]);
+
+ this.id = id;
+ this.node = table
+ this.options = options;
+
+ var sorting = this.getActiveSortState();
+
+ for (var i = 0; i < captions.length; i++) {
+ if (captions[i] == null)
+ continue;
+
+ var th = E('th', { 'class': 'th' }, [ captions[i] ]);
+
+ if (typeof(options.captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(th.classList, L.toArray(options.captionClasses[i]));
+
+ if (options.sortable !== false && (typeof(options.sortable) != 'object' || options.sortable[i] !== false)) {
+ th.setAttribute('data-sortable-row', true);
+
+ if (sorting && sorting[0] == i)
+ th.setAttribute('data-sort-direction', sorting[1] ? 'desc' : 'asc');
+ }
+
+ table.firstElementChild.appendChild(th);
+ }
+
+ if (placeholder) {
+ var trow = table.appendChild(E('tr', { 'class': 'tr placeholder' })),
+ td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
+ }
+
+ DOMTokenList.prototype.add.apply(table.classList, L.toArray(options.classes));
+ },
+
+ update: function(data, placeholder) {
+ var placeholder = placeholder || this.options.placeholder || _('No data', 'empty table placeholder'),
+ sorting = this.getActiveSortState();
+
+ if (!Array.isArray(data))
+ return;
+
+ this.data = data;
+ this.placeholder = placeholder;
+
+ var n = 0,
+ rows = this.node.querySelectorAll('tr, .tr'),
+ trows = [],
+ headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th, .th')),
+ captionClasses = this.options.captionClasses,
+ trTag = (rows[0] && rows[0].nodeName == 'DIV') ? 'div' : 'tr',
+ tdTag = (headings[0] && headings[0].nodeName == 'DIV') ? 'div' : 'td';
+
+ if (sorting) {
+ var list = data.map(L.bind(function(row) {
+ return [ this.deriveSortKey(row[sorting[0]], sorting[0]), row ];
+ }, this));
+
+ list.sort(function(a, b) {
+ return sorting[1]
+ ? -L.naturalCompare(a[0], b[0])
+ : L.naturalCompare(a[0], b[0]);
+ });
+
+ data.length = 0;
+
+ list.forEach(function(item) {
+ data.push(item[1]);
+ });
+
+ headings.forEach(function(th, i) {
+ if (i == sorting[0])
+ th.setAttribute('data-sort-direction', sorting[1] ? 'desc' : 'asc');
+ else
+ th.removeAttribute('data-sort-direction');
+ });
+ }
+
+ data.forEach(function(row) {
+ trows[n] = E(trTag, { 'class': 'tr' });
+
+ for (var i = 0; i < headings.length; i++) {
+ var text = (headings[i].innerText || '').trim();
+ var raw_val = Array.isArray(row[i]) ? row[i][0] : null;
+ var disp_val = Array.isArray(row[i]) ? row[i][1] : row[i];
+ var td = trows[n].appendChild(E(tdTag, {
+ 'class': 'td',
+ 'data-title': (text !== '') ? text : null,
+ 'data-value': raw_val
+ }, (disp_val != null) ? ((disp_val instanceof DocumentFragment) ? disp_val.cloneNode(true) : disp_val) : ''));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[i]));
+
+ if (!td.classList.contains('cbi-section-actions'))
+ headings[i].setAttribute('data-sortable-row', true);
+ }
+
+ trows[n].classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
+ });
+
+ for (var i = 0; i < n; i++) {
+ if (rows[i+1])
+ this.node.replaceChild(trows[i], rows[i+1]);
+ else
+ this.node.appendChild(trows[i]);
+ }
+
+ while (rows[++n])
+ this.node.removeChild(rows[n]);
+
+ if (placeholder && this.node.firstElementChild === this.node.lastElementChild) {
+ var trow = this.node.appendChild(E(trTag, { 'class': 'tr placeholder' })),
+ td = trow.appendChild(E(tdTag, { 'class': 'td' }, placeholder));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
+ }
+
+ return this.node;
+ },
+
+ render: function() {
+ return this.node;
+ },
+
+ /** @private */
+ initFromMarkup: function(node) {
+ if (!dom.elem(node))
+ node = document.querySelector(node);
+
+ if (!node)
+ throw 'Invalid table selector';
+
+ var options = {},
+ headrow = node.querySelector('tr, .tr');
+
+ if (!headrow)
+ return;
+
+ options.id = node.id;
+ options.classes = [].slice.call(node.classList).filter(function(c) { return c != 'table' });
+ options.sortable = [];
+ options.captionClasses = [];
+
+ headrow.querySelectorAll('th, .th').forEach(function(th, i) {
+ options.sortable[i] = !th.classList.contains('cbi-section-actions');
+ options.captionClasses[i] = [].slice.call(th.classList).filter(function(c) { return c != 'th' });
+ });
+
+ headrow.addEventListener('click', UI.prototype.createHandlerFn(this, 'handleSort'));
+
+ this.id = node.id;
+ this.node = node;
+ this.options = options;
+ },
+
+ /** @private */
+ deriveSortKey: function(value, index) {
+ var opts = this.options || {},
+ hint, m;
+
+ if (opts.sortable == true || opts.sortable == null)
+ hint = 'auto';
+ else if (typeof( opts.sortable) == 'object')
+ hint = opts.sortable[index];
+
+ if (dom.elem(value)) {
+ if (value.hasAttribute('data-value'))
+ value = value.getAttribute('data-value');
+ else
+ value = (value.innerText || '').trim();
+ }
+
+ switch (hint || 'auto') {
+ case true:
+ case 'auto':
+ m = /^([0-9a-fA-F:.]+)(?:\/([0-9a-fA-F:.]+))?$/.exec(value);
+
+ if (m) {
+ var addr, mask;
+
+ addr = validation.parseIPv6(m[1]);
+ mask = m[2] ? validation.parseIPv6(m[2]) : null;
+
+ if (addr && mask != null)
+ return '%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x'.format(
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7],
+ mask[0], mask[1], mask[2], mask[3], mask[4], mask[5], mask[6], mask[7]
+ );
+ else if (addr)
+ return '%04x%04x%04x%04x%04x%04x%04x%04x%02x'.format(
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7],
+ m[2] ? +m[2] : 128
+ );
+
+ addr = validation.parseIPv4(m[1]);
+ mask = m[2] ? validation.parseIPv4(m[2]) : null;
+
+ if (addr && mask != null)
+ return '%03d%03d%03d%03d%03d%03d%03d%03d'.format(
+ addr[0], addr[1], addr[2], addr[3],
+ mask[0], mask[1], mask[2], mask[3]
+ );
+ else if (addr)
+ return '%03d%03d%03d%03d%02d'.format(
+ addr[0], addr[1], addr[2], addr[3],
+ m[2] ? +m[2] : 32
+ );
+ }
+
+ m = /^(?:(\d+)d )?(\d+)h (\d+)m (\d+)s$/.exec(value);
+
+ if (m)
+ return '%05d%02d%02d%02d'.format(+m[1], +m[2], +m[3], +m[4]);
+
+ m = /^(\d+)\b(\D*)$/.exec(value);
+
+ if (m)
+ return '%010d%s'.format(+m[1], m[2]);
+
+ return String(value);
+
+ case 'ignorecase':
+ return String(value).toLowerCase();
+
+ case 'numeric':
+ return +value;
+
+ default:
+ return String(value);
+ }
+ },
+
+ /** @private */
+ getActiveSortState: function() {
+ if (this.sortState)
+ return this.sortState;
+
+ if (!this.options.id)
+ return null;
+
+ var page = document.body.getAttribute('data-page'),
+ key = page + '.' + this.options.id,
+ state = session.getLocalData('tablesort');
+
+ if (L.isObject(state) && Array.isArray(state[key]))
+ return state[key];
+
+ return null;
+ },
+
+ /** @private */
+ setActiveSortState: function(index, descending) {
+ this.sortState = [ index, descending ];
+
+ if (!this.options.id)
+ return;
+
+ var page = document.body.getAttribute('data-page'),
+ key = page + '.' + this.options.id,
+ state = session.getLocalData('tablesort');
+
+ if (!L.isObject(state))
+ state = {};
+
+ state[key] = this.sortState;
+
+ session.setLocalData('tablesort', state);
+ },
+
+ /** @private */
+ handleSort: function(ev) {
+ if (!ev.target.matches('th[data-sortable-row]'))
+ return;
+
+ var index, direction;
+
+ this.node.firstElementChild.querySelectorAll('th, .th').forEach(function(th, i) {
+ if (th === ev.target) {
+ index = i;
+ direction = th.getAttribute('data-sort-direction') == 'asc';
+ }
+ });
+
+ this.setActiveSortState(index, direction);
+ this.update(this.data, this.placeholder);
+ }
+});
+
/**
* @class ui
* @memberof LuCI
@@ -3153,8 +3494,17 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
__init__: function() {
modalDiv = document.body.appendChild(
- dom.create('div', { id: 'modal_overlay' },
- dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
+ dom.create('div', {
+ id: 'modal_overlay',
+ tabindex: -1,
+ keydown: this.cancelModal
+ }, [
+ dom.create('div', {
+ class: 'modal',
+ role: 'dialog',
+ 'aria-modal': true
+ })
+ ]));
tooltipDiv = document.body.appendChild(
dom.create('div', { class: 'cbi-tooltip' }));
@@ -3184,7 +3534,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* be opened. Invoking showModal() while a modal dialog is already open will
* replace the open dialog with a new one having the specified contents.
*
- * Additional CSS class names may be passed to influence the appearence of
+ * Additional CSS class names may be passed to influence the appearance of
* the dialog. Valid values for the classes depend on the underlying theme.
*
* @see LuCI.dom.content
@@ -3192,7 +3542,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* @param {string} [title]
* The title of the dialog. If `null`, no title element will be rendered.
*
- * @param {*} contents
+ * @param {*} children
* The contents to add to the modal dialog. This should be a DOM node or
* a document fragment in most cases. The value is passed as-is to the
* `dom.content()` function - refer to its documentation for applicable
@@ -3218,6 +3568,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
document.body.classList.add('modal-overlay-active');
modalDiv.scrollTop = 0;
+ modalDiv.focus();
return dlg;
},
@@ -3234,6 +3585,17 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
*/
hideModal: function() {
document.body.classList.remove('modal-overlay-active');
+ modalDiv.blur();
+ },
+
+ /** @private */
+ cancelModal: function(ev) {
+ if (ev.key == 'Escape') {
+ var btn = modalDiv.querySelector('.right > button, .right > .btn');
+
+ if (btn)
+ btn.click();
+ }
},
/** @private */
@@ -3304,11 +3666,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* Add a notification banner at the top of the current view.
*
* A notification banner is an alert message usually displayed at the
- * top of the current view, spanning the entire availibe width.
+ * top of the current view, spanning the entire available width.
* Notification banners will stay in place until dismissed by the user.
* Multiple banners may be shown at the same time.
*
- * Additional CSS class names may be passed to influence the appearence of
+ * Additional CSS class names may be passed to influence the appearance of
* the banner. Valid values for the classes depend on the underlying theme.
*
* @see LuCI.dom.content
@@ -3317,7 +3679,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* The title of the notification banner. If `null`, no title element
* will be rendered.
*
- * @param {*} contents
+ * @param {*} children
* The contents to add to the notification banner. This should be a DOM
* node or a document fragment in most cases. The value is passed as-is
* to the `dom.content()` function - refer to its documentation for
@@ -3368,7 +3730,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
},
/**
- * Display or update an header area indicator.
+ * Display or update a header area indicator.
*
* An indicator is a small label displayed in the header area of the screen
* providing few amounts of status information such as item counts or state
@@ -3395,7 +3757,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* Note that this parameter only applies to new indicators, when updating
* existing labels it is ignored.
*
- * @param {string} [style=active]
+ * @param {"active"|"inactive"} [style=active]
* The indicator style to use. May be either `active` or `inactive`.
*
* @returns {boolean}
@@ -3438,7 +3800,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
},
/**
- * Remove an header area indicator.
+ * Remove a header area indicator.
*
* This function removes the given indicator label from the header indicator
* area. When the given indicator is not found, this function does nothing.
@@ -3472,7 +3834,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* subsequently wrapped into a `<span class="nowrap">` element.
*
* The resulting `<span>` element tuples are joined by the given separators
- * to form the final markup which is appened to the given parent DOM node.
+ * to form the final markup which is appended to the given parent DOM node.
*
* @param {Node} node
* The parent DOM node to append the markup to. Any previous child elements
@@ -3799,7 +4161,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* @param {string} path
* The remote file path to upload the local file to.
*
- * @param {Node} [progessStatusNode]
+ * @param {Node} [progressStatusNode]
* An optional DOM text node whose content text is set to the progress
* percentage value during file upload.
*
@@ -3849,7 +4211,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
'class': 'btn',
'click': function() {
UI.prototype.hideModal();
- rejectFn(new Error('Upload has been cancelled'));
+ rejectFn(new Error(_('Upload has been cancelled')));
}
}, [ _('Cancel') ]),
' ',
@@ -3913,7 +4275,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
/**
* Perform a device connectivity test.
*
- * Attempt to fetch a well known ressource from the remote device via HTTP
+ * Attempt to fetch a well known resource from the remote device via HTTP
* in order to test connectivity. This function is mainly useful to wait
* for the router to come back online after a reboot or reconfiguration.
*
@@ -3921,7 +4283,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* The protocol to use for fetching the resource. May be either `http`
* (the default) or `https`.
*
- * @param {string} [host=window.location.host]
+ * @param {string} [ipaddr=window.location.host]
* Override the host address to probe. By default the current host as seen
* in the address bar is probed.
*
@@ -4010,7 +4372,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
*
* @instance
* @memberof LuCI.ui.changes
- * @param {number} numChanges
+ * @param {number} n
* The number of changes to indicate.
*/
setIndicator: function(n) {
@@ -4089,10 +4451,16 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
'class': 'btn',
'click': UI.prototype.hideModal
}, [ _('Close') ]), ' ',
- E('button', {
- 'class': 'cbi-button cbi-button-positive important',
- 'click': L.bind(this.apply, this, true)
- }, [ _('Save & Apply') ]), ' ',
+ new UIComboButton('0', {
+ 0: [ _('Save & Apply') ],
+ 1: [ _('Apply unchecked') ]
+ }, {
+ classes: {
+ 0: 'btn cbi-button cbi-button-positive important',
+ 1: 'btn cbi-button cbi-button-negative important'
+ },
+ click: L.bind(function(ev, mode) { this.apply(mode == '0') }, this)
+ }).render(), ' ',
E('button', {
'class': 'cbi-button cbi-button-reset',
'click': L.bind(this.revert, this)
@@ -4162,6 +4530,26 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
},
/** @private */
+ checkConnectivityAffected: function() {
+ return L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr', null, 'json')).then(L.bind(function(info) {
+ if (L.isObject(info) && Array.isArray(info.inbound_interfaces)) {
+ for (var i = 0; i < info.inbound_interfaces.length; i++) {
+ var iif = info.inbound_interfaces[i];
+
+ for (var j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
+ var chg = this.changes.network[j];
+
+ if (chg[0] == 'set' && chg[1] == iif && (chg[2] == 'proto' || chg[2] == 'ipaddr' || chg[2] == 'netmask'))
+ return iif;
+ }
+ }
+ }
+
+ return null;
+ }, this));
+ },
+
+ /** @private */
rollback: function(checked) {
if (checked) {
this.displayStatus('warning spinning',
@@ -4198,7 +4586,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
method: 'post',
timeout: L.env.apply_timeout * 1000,
query: { sid: L.env.sessionid, token: L.env.token }
- }).then(call);
+ }).then(call, call.bind(null, { status: 0 }, null, 0));
}, delay);
};
@@ -4298,35 +4686,65 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
this.displayStatus('notice spinning',
E('p', _('Starting configuration apply…')));
- request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
- method: 'post',
- query: { sid: L.env.sessionid, token: L.env.token }
- }).then(function(r) {
- if (r.status === (checked ? 200 : 204)) {
- var tok = null; try { tok = r.json(); } catch(e) {}
- if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
- UI.prototype.changes.confirm_auth = tok;
-
- UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
- }
- else if (checked && r.status === 204) {
- UI.prototype.changes.displayStatus('notice',
- E('p', _('There are no changes to apply')));
+ (new Promise(function(resolveFn, rejectFn) {
+ if (!checked)
+ return resolveFn(false);
+
+ UI.prototype.changes.checkConnectivityAffected().then(function(affected) {
+ if (!affected)
+ return resolveFn(true);
+
+ UI.prototype.changes.displayStatus('warning', [
+ E('h4', _('Connectivity change')),
+ E('p', _('The network access to this device could be interrupted by changing settings of the "%h" interface.').format(affected)),
+ E('p', _('If the IP address used to access LuCI changes, a <strong>manual reconnect to the new IP</strong> is required within %d seconds to confirm the settings, otherwise modifications will be reverted.').format(L.env.apply_rollback)),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': rejectFn,
+ }, [ _('Cancel') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': resolveFn.bind(null, true)
+ }, [ _('Apply with revert after connectivity loss') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-negative important',
+ 'click': resolveFn.bind(null, false)
+ }, [ _('Apply and keep settings') ])
+ ])
+ ]);
+ });
+ })).then(function(checked) {
+ request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
+ method: 'post',
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(function(r) {
+ if (r.status === (checked ? 200 : 204)) {
+ var tok = null; try { tok = r.json(); } catch(e) {}
+ if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
+ UI.prototype.changes.confirm_auth = tok;
+
+ UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
+ }
+ else if (checked && r.status === 204) {
+ UI.prototype.changes.displayStatus('notice',
+ E('p', _('There are no changes to apply')));
- window.setTimeout(function() {
- UI.prototype.changes.displayStatus(false);
- }, L.env.apply_display * 1000);
- }
- else {
- UI.prototype.changes.displayStatus('warning',
- E('p', _('Apply request failed with status <code>%h</code>')
- .format(r.responseText || r.statusText || r.status)));
+ window.setTimeout(function() {
+ UI.prototype.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ UI.prototype.changes.displayStatus('warning',
+ E('p', _('Apply request failed with status <code>%h</code>')
+ .format(r.responseText || r.statusText || r.status)));
- window.setTimeout(function() {
- UI.prototype.changes.displayStatus(false);
- }, L.env.apply_display * 1000);
- }
- });
+ window.setTimeout(function() {
+ UI.prototype.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ }, this.displayStatus.bind(this, false));
},
/**
@@ -4495,7 +4913,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
*
* By instantiating the view class, its corresponding contents are
* rendered and included into the view area. Any runtime errors are
- * catched and rendered using [LuCI.error()]{@link LuCI#error}.
+ * caught and rendered using [LuCI.error()]{@link LuCI#error}.
*
* @param {string} path
* The view path to render.
@@ -4519,6 +4937,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
menu: UIMenu,
+ Table: UITable,
+
AbstractElement: UIElement,
/* Widgets */
diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js
index 6dddf964fb..791a84823d 100644
--- a/modules/luci-base/htdocs/luci-static/resources/validation.js
+++ b/modules/luci-base/htdocs/luci-static/resources/validation.js
@@ -426,6 +426,15 @@ var ValidatorFactory = baseclass.extend({
return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
},
+ netdevname: function() {
+ var v = this.value;
+
+ if (v == '.' || v == '..')
+ return this.assert(false, _('valid network device name, not "." or ".."'));
+
+ return this.assert(v.match(/^[^:\/%\s]{1,15}$/), _('valid network device name between 1 and 15 characters not containing ":", "/", "%" or spaces'));
+ },
+
range: function(min, max) {
var val = this.factory.parseDecimal(this.value);
return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));