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/form.js136
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js375
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/network.js4
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/rpc.js11
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js3
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/uci.js38
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js202
7 files changed, 586 insertions, 183 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
index 2b02066a40..c65cb04b13 100644
--- a/modules/luci-base/htdocs/luci-static/resources/form.js
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -1,11 +1,19 @@
'use strict';
'require ui';
'require uci';
+'require rpc';
'require dom';
'require baseclass';
var scope = this;
+var callSessionAccess = rpc.declare({
+ object: 'session',
+ method: 'access',
+ params: [ 'scope', 'object', 'function' ],
+ expect: { 'access': false }
+});
+
var CBIJSONConfig = baseclass.extend({
__init__: function(data) {
data = Object.assign({}, data);
@@ -364,6 +372,20 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
},
/**
+ * Toggle readonly state of the form.
+ *
+ * If set to `true`, the Map instance is marked readonly and any form
+ * option elements added to it will inherit the readonly state.
+ *
+ * If left unset, the Map will test the access permission of the primary
+ * uci configuration upon loading and mark the form readonly if no write
+ * permissions are granted.
+ *
+ * @name LuCI.form.Map.prototype#readonly
+ * @type boolean
+ */
+
+ /**
* Find all DOM nodes within this Map which match the given search
* parameters. This function is essentially a convenience wrapper around
* `querySelectorAll()`.
@@ -509,8 +531,17 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
* an error.
*/
load: function() {
- return this.data.load(this.parsechain || [ this.config ])
- .then(this.loadChildren.bind(this));
+ var doCheckACL = (!(this instanceof CBIJSONMap) && this.readonly == null);
+
+ return Promise.all([
+ doCheckACL ? callSessionAccess('uci', this.config, 'write') : true,
+ this.data.load(this.parsechain || [ this.config ])
+ ]).then(L.bind(function(res) {
+ if (res[0] === false)
+ this.readonly = true;
+
+ return this.loadChildren();
+ }, this));
},
/**
@@ -564,11 +595,18 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
.then(this.data.save.bind(this.data))
.then(this.load.bind(this))
.catch(function(e) {
- if (!silent)
- alert('Cannot save due to invalid values');
+ if (!silent) {
+ ui.showModal(_('Save error'), [
+ E('p', {}, [ _('An error occurred while saving the form:') ]),
+ E('p', {}, [ E('em', { 'style': 'white-space:pre' }, [ e.message ]) ]),
+ E('div', { 'class': 'right' }, [
+ E('button', { 'click': ui.hideModal }, [ _('Dismiss') ])
+ ])
+ ]);
+ }
- return Promise.reject();
- }).finally(this.renderContents.bind(this));
+ return Promise.reject(e);
+ }).then(this.renderContents.bind(this));
},
/**
@@ -1302,6 +1340,19 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
*/
/**
+ * Make option element readonly.
+ *
+ * This property defaults to the readonly state of the parent form element.
+ * When set to `true`, the underlying widget is rendered in disabled state,
+ * means its contents cannot be changed and the widget cannot be interacted
+ * with.
+ *
+ * @name LuCI.form.AbstractValue.prototype#readonly
+ * @type boolean
+ * @default false
+ */
+
+ /**
* Override the cell width of a table or grid section child option.
*
* If the property is set to a numeric value, it is treated as pixel width
@@ -1754,8 +1805,10 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
cval = this.cfgvalue(section_id),
fval = active ? this.formvalue(section_id) : null;
- if (active && !this.isValid(section_id))
- return Promise.reject();
+ if (active && !this.isValid(section_id)) {
+ var title = this.stripTags(this.title).trim();
+ return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
+ }
if (fval != '' && fval != null) {
if (this.forcewrite || !isEqual(cval, fval))
@@ -1766,8 +1819,8 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa
return Promise.resolve(this.remove(section_id));
}
else if (!isEqual(cval, fval)) {
- console.log('This should have been catched by isValid()');
- return Promise.reject();
+ var title = this.stripTags(this.title).trim();
+ return Promise.reject(new TypeError(_('Option "%s" must not be empty.').format(title || this.option)));
}
}
@@ -1951,13 +2004,15 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
createEl.appendChild(E('button', {
'class': 'cbi-button cbi-button-add',
'title': btn_title || _('Add'),
- 'click': ui.createHandlerFn(this, 'handleAdd')
+ 'click': ui.createHandlerFn(this, 'handleAdd'),
+ 'disabled': this.map.readonly || null
}, [ btn_title || _('Add') ]));
}
else {
var nameEl = E('input', {
'type': 'text',
- 'class': 'cbi-section-create-name'
+ 'class': 'cbi-section-create-name',
+ 'disabled': this.map.readonly || null
});
dom.append(createEl, [
@@ -1972,7 +2027,8 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
return;
return this.handleAdd(ev, nameEl.value);
- })
+ }),
+ 'disabled': this.map.readonly || null
})
]);
@@ -2015,7 +2071,8 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio
'class': 'cbi-button',
'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
'data-section-id': cfgsections[i],
- 'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
+ 'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i]),
+ 'disabled': this.map.readonly || null
}, [ _('Delete') ])));
}
@@ -2391,7 +2448,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
E('div', {
'title': _('Drag to reorder'),
'class': 'btn cbi-button drag-handle center',
- 'style': 'cursor:move'
+ 'style': 'cursor:move',
+ 'disabled': this.map.readonly || null
}, '☰')
]);
}
@@ -2432,7 +2490,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
E('button', {
'title': btn_title || _('Delete'),
'class': 'cbi-button cbi-button-remove',
- 'click': ui.createHandlerFn(this, 'handleRemove', section_id)
+ 'click': ui.createHandlerFn(this, 'handleRemove', section_id),
+ 'disabled': this.map.readonly || null
}, [ btn_title || _('Delete') ])
);
}
@@ -2583,6 +2642,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
s = m.section(CBINamedSection, section_id, this.sectiontype);
m.parent = parent;
+ m.readonly = parent.readonly;
s.tabs = this.tabs;
s.tab_names = this.tab_names;
@@ -2630,7 +2690,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
}, [ _('Dismiss') ]), ' ',
E('button', {
'class': 'cbi-button cbi-button-positive important',
- 'click': ui.createHandlerFn(this, 'handleModalSave', m)
+ 'click': ui.createHandlerFn(this, 'handleModalSave', m),
+ 'disabled': m.readonly || null
}, [ _('Save') ])
])
], 'cbi-modal');
@@ -2917,7 +2978,8 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
E('div', { 'class': 'cbi-section-remove right' },
E('button', {
'class': 'cbi-button',
- 'click': ui.createHandlerFn(this, 'handleRemove')
+ 'click': ui.createHandlerFn(this, 'handleRemove'),
+ 'disabled': this.map.readonly || null
}, [ _('Delete') ])));
}
@@ -2932,7 +2994,8 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio
sectionEl.appendChild(
E('button', {
'class': 'cbi-button cbi-button-add',
- 'click': ui.createHandlerFn(this, 'handleAdd')
+ 'click': ui.createHandlerFn(this, 'handleAdd'),
+ 'disabled': this.map.readonly || null
}, [ _('Add') ]));
}
@@ -3126,7 +3189,8 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
optional: this.optional || this.rmempty,
datatype: this.datatype,
select_placeholder: this.placeholder || placeholder,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
}
else {
@@ -3136,7 +3200,8 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
}
@@ -3191,7 +3256,8 @@ var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
@@ -3256,7 +3322,8 @@ var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
sort: this.keylist,
optional: this.optional,
placeholder: this.placeholder,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
@@ -3327,7 +3394,8 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
id: this.cbid(section_id),
value_enabled: this.enabled,
value_disabled: this.disabled,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
@@ -3366,8 +3434,10 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
if (this.isActive(section_id)) {
var fval = this.formvalue(section_id);
- if (!this.isValid(section_id))
- return Promise.reject();
+ if (!this.isValid(section_id)) {
+ var title = this.stripTags(this.title).trim();
+ return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
+ }
if (fval == this.default && (this.optional || this.rmempty))
return Promise.resolve(this.remove(section_id));
@@ -3453,7 +3523,8 @@ var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.protot
select_placeholder: this.placeholder,
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || -1,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
@@ -3545,7 +3616,8 @@ var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
cols: this.cols,
rows: this.rows,
wrap: this.wrap,
- validate: L.bind(this.validate, this, section_id)
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
@@ -3615,7 +3687,7 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */
hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
outputEl = E('div');
- if (this.href)
+ if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly))
outputEl.appendChild(E('a', { 'href': this.href }));
dom.append(outputEl.lastChild || outputEl,
@@ -3736,7 +3808,8 @@ var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype
ev.currentTarget.parentNode.nextElementSibling.value = value;
return this.map.save();
- }, section_id)
+ }, section_id),
+ 'disabled': ((this.readonly != null) ? this.readonly : this.map.readonly) || null
}, [ btn_title ])
]);
else
@@ -3911,7 +3984,8 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
show_hidden: this.show_hidden,
enable_upload: this.enable_upload,
enable_remove: this.enable_remove,
- root_directory: this.root_directory
+ root_directory: this.root_directory,
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return browserEl.render();
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 5984ad184a..c4f998a406 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -12,6 +12,8 @@
(function(window, document, undefined) {
'use strict';
+ var env = {};
+
/* Object.assign polyfill for IE */
if (typeof Object.assign !== 'function') {
Object.defineProperty(Object, 'assign', {
@@ -1069,7 +1071,7 @@
*/
add: function(fn, interval) {
if (interval == null || interval <= 0)
- interval = window.L ? window.L.env.pollinterval : null;
+ interval = env.pollinterval || null;
if (isNaN(interval) || typeof(fn) != 'function')
throw new TypeError('Invalid argument to LuCI.poll.add()');
@@ -1211,7 +1213,7 @@
* To import the class in views, use `'require dom'`, to import it in
* external JavaScript, use `L.require("dom").then(...)`.
*/
- var DOM = Class.singleton(/* @lends LuCI.dom.prototype */ {
+ var DOM = Class.singleton(/** @lends LuCI.dom.prototype */ {
__name__: 'LuCI.dom',
/**
@@ -1716,7 +1718,7 @@
*/
bindClassInstance: function(node, inst) {
if (!(inst instanceof Class))
- L.error('TypeError', 'Argument must be a class instance');
+ LuCI.prototype.error('TypeError', 'Argument must be a class instance');
return this.data(node, '_class', inst);
},
@@ -1824,6 +1826,101 @@
});
/**
+ * @class session
+ * @memberof LuCI
+ * @hideconstructor
+ * @classdesc
+ *
+ * The `session` class provides various session related functionality.
+ */
+ var Session = Class.singleton(/** @lends LuCI.session.prototype */ {
+ __name__: 'LuCI.session',
+
+ /**
+ * Retrieve the current session ID.
+ *
+ * @returns {string}
+ * Returns the current session ID.
+ */
+ getID: function() {
+ return env.sessionid || '00000000000000000000000000000000';
+ },
+
+ /**
+ * Retrieve data from the local session storage.
+ *
+ * @param {string} [key]
+ * The key to retrieve from the session data store. If omitted, all
+ * session data will be returned.
+ *
+ * @returns {*}
+ * Returns the stored session data or `null` if the given key wasn't
+ * found.
+ */
+ getLocalData: function(key) {
+ try {
+ var sid = this.getID(),
+ item = 'luci-session-store',
+ data = JSON.parse(window.sessionStorage.getItem(item));
+
+ if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
+ data = {};
+ data[sid] = {};
+ }
+
+ if (key != null)
+ return data[sid].hasOwnProperty(key) ? data[sid][key] : null;
+
+ return data[sid];
+ }
+ catch (e) {
+ return (key != null) ? null : {};
+ }
+ },
+
+ /**
+ * Set data in the local session storage.
+ *
+ * @param {string} key
+ * The key to set in the session data store.
+ *
+ * @param {*} value
+ * The value to store. It will be internally converted to JSON before
+ * being put in the session store.
+ *
+ * @returns {boolean}
+ * Returns `true` if the data could be stored or `false` on error.
+ */
+ setLocalData: function(key, value) {
+ if (key == null)
+ return false;
+
+ try {
+ var sid = this.getID(),
+ item = 'luci-session-store',
+ data = JSON.parse(window.sessionStorage.getItem(item));
+
+ if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
+ data = {};
+ data[sid] = {};
+ }
+
+ if (value != null)
+ data[sid][key] = value;
+ else
+ delete data[sid][key];
+
+ window.sessionStorage.setItem(item, JSON.stringify(data));
+
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ });
+
+ /**
* @class view
* @memberof LuCI
* @hideconstructor
@@ -1832,7 +1929,7 @@
* The `view` class forms the basis of views and provides a standard
* set of methods to inherit from.
*/
- var View = Class.extend(/* @lends LuCI.view.prototype */ {
+ var View = Class.extend(/** @lends LuCI.view.prototype */ {
__name__: 'LuCI.view',
__init__: function() {
@@ -1841,13 +1938,13 @@
DOM.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
return Promise.resolve(this.load())
- .then(L.bind(this.render, this))
- .then(L.bind(function(nodes) {
+ .then(LuCI.prototype.bind(this.render, this))
+ .then(LuCI.prototype.bind(function(nodes) {
var vp = document.getElementById('view');
DOM.content(vp, nodes);
DOM.append(vp, this.addFooter());
- }, this)).catch(L.error);
+ }, this)).catch(LuCI.prototype.error);
},
/**
@@ -1981,7 +2078,7 @@
*/
handleSaveApply: function(ev, mode) {
return this.handleSave(ev).then(function() {
- L.ui.changes.apply(mode == '0');
+ classes.ui.changes.apply(mode == '0');
});
},
@@ -2051,9 +2148,25 @@
* methods are overwritten with `null`.
*/
addFooter: function() {
- var footer = E([]);
+ var footer = E([]),
+ vp = document.getElementById('view'),
+ hasmap = false,
+ readonly = true;
+
+ vp.querySelectorAll('.cbi-map').forEach(function(map) {
+ var m = DOM.findClassInstance(map);
+ if (m) {
+ hasmap = true;
+
+ if (!m.readonly)
+ readonly = false;
+ }
+ });
- var saveApplyBtn = this.handleSaveApply ? new L.ui.ComboButton('0', {
+ if (!hasmap)
+ readonly = !LuCI.prototype.hasViewPermission();
+
+ var saveApplyBtn = this.handleSaveApply ? new classes.ui.ComboButton('0', {
0: [ _('Save & Apply') ],
1: [ _('Apply unchecked') ]
}, {
@@ -2061,7 +2174,8 @@
0: 'btn cbi-button cbi-button-apply important',
1: 'btn cbi-button cbi-button-negative important'
},
- click: L.ui.createHandlerFn(this, 'handleSaveApply')
+ click: classes.ui.createHandlerFn(this, 'handleSaveApply'),
+ disabled: readonly || null
}).render() : E([]);
if (this.handleSaveApply || this.handleSave || this.handleReset) {
@@ -2069,11 +2183,13 @@
saveApplyBtn, ' ',
this.handleSave ? E('button', {
'class': 'cbi-button cbi-button-save',
- 'click': L.ui.createHandlerFn(this, 'handleSave')
+ 'click': classes.ui.createHandlerFn(this, 'handleSave'),
+ 'disabled': readonly || null
}, [ _('Save') ]) : '', ' ',
this.handleReset ? E('button', {
'class': 'cbi-button cbi-button-reset',
- 'click': L.ui.createHandlerFn(this, 'handleReset')
+ 'click': classes.ui.createHandlerFn(this, 'handleReset'),
+ 'disabled': readonly || null
}, [ _('Reset') ]) : ''
]));
}
@@ -2087,7 +2203,8 @@
domParser = null,
originalCBIInit = null,
rpcBaseURL = null,
- sysFeatures = null;
+ sysFeatures = null,
+ preloadClasses = null;
/* "preload" builtin classes to make the available via require */
var classes = {
@@ -2095,41 +2212,30 @@
dom: DOM,
poll: Poll,
request: Request,
+ session: Session,
view: View
};
var LuCI = Class.extend(/** @lends LuCI.prototype */ {
__name__: 'LuCI',
- __init__: function(env) {
+ __init__: function(setenv) {
document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
- if (env.base_url == null || env.base_url == '') {
+ if (setenv.base_url == null || setenv.base_url == '') {
var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
if (m) {
- env.base_url = m[1];
- env.resource_version = m[2];
+ setenv.base_url = m[1];
+ setenv.resource_version = m[2];
}
}
});
- if (env.base_url == null)
+ if (setenv.base_url == null)
this.error('InternalError', 'Cannot find url of luci.js');
- env.cgi_base = env.scriptname.replace(/\/[^\/]+$/, '');
+ setenv.cgi_base = setenv.scriptname.replace(/\/[^\/]+$/, '');
- Object.assign(this.env, env);
-
- document.addEventListener('poll-start', function(ev) {
- document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
- e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
- });
- });
-
- document.addEventListener('poll-stop', function(ev) {
- document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
- e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
- });
- });
+ Object.assign(env, setenv);
var domReady = new Promise(function(resolveFn, rejectFn) {
document.addEventListener('DOMContentLoaded', resolveFn);
@@ -2239,12 +2345,13 @@
*/
error: function(type, fmt /*, ...*/) {
try {
- L.raise.apply(L, Array.prototype.slice.call(arguments));
+ LuCI.prototype.raise.apply(LuCI.prototype,
+ Array.prototype.slice.call(arguments));
}
catch (e) {
if (!e.reported) {
- if (L.ui)
- L.ui.addNotification(e.name || _('Runtime error'),
+ if (classes.ui)
+ classes.ui.addNotification(e.name || _('Runtime error'),
E('pre', {}, e.message), 'danger');
else
DOM.content(document.querySelector('#maincontent'),
@@ -2323,19 +2430,19 @@
if (classes[name] != null) {
/* Circular dependency */
if (from.indexOf(name) != -1)
- L.raise('DependencyError',
+ LuCI.prototype.raise('DependencyError',
'Circular dependency: class "%s" depends on "%s"',
name, from.join('" which depends on "'));
return Promise.resolve(classes[name]);
}
- url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
+ url = '%s/%s.js%s'.format(env.base_url, name.replace(/\./g, '/'), (env.resource_version ? '?v=' + env.resource_version : ''));
from = [ name ].concat(from);
var compileClass = function(res) {
if (!res.ok)
- L.raise('NetworkError',
+ LuCI.prototype.raise('NetworkError',
'HTTP error %d while loading class file "%s"', res.status, url);
var source = res.text(),
@@ -2360,7 +2467,7 @@
if (m) {
var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
- depends.push(L.require(dep, from));
+ depends.push(LuCI.prototype.require(dep, from));
args += ', ' + as;
}
else if (!strictmatch.exec(s)) {
@@ -2386,7 +2493,7 @@
.format(args, source, res.url));
}
catch (error) {
- L.raise('SyntaxError', '%s\n in %s:%s',
+ LuCI.prototype.raise('SyntaxError', '%s\n in %s:%s',
error.message, res.url, error.lineNumber || '?');
}
@@ -2394,7 +2501,7 @@
_class = _factory.apply(_factory, [window, document, L].concat(instances));
if (!Class.isSubclass(_class))
- L.error('TypeError', '"%s" factory yields invalid constructor', name);
+ LuCI.prototype.error('TypeError', '"%s" factory yields invalid constructor', name);
if (_class.displayName == 'AnonymousClass')
_class.displayName = toCamelCase(name + 'Class');
@@ -2423,26 +2530,18 @@
/* DOM setup */
probeRPCBaseURL: function() {
- if (rpcBaseURL == null) {
- try {
- rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
- }
- catch (e) { }
- }
+ if (rpcBaseURL == null)
+ rpcBaseURL = Session.getLocalData('rpcBaseURL');
if (rpcBaseURL == null) {
var rpcFallbackURL = this.url('admin/ubus');
- rpcBaseURL = Request.get(this.env.ubuspath).then(function(res) {
- return (rpcBaseURL = (res.status == 400) ? L.env.ubuspath : rpcFallbackURL);
+ rpcBaseURL = Request.get(env.ubuspath).then(function(res) {
+ return (rpcBaseURL = (res.status == 400) ? env.ubuspath : rpcFallbackURL);
}, function() {
return (rpcBaseURL = rpcFallbackURL);
}).then(function(url) {
- try {
- window.sessionStorage.setItem('rpcBaseURL', url);
- }
- catch (e) { }
-
+ Session.setLocalData('rpcBaseURL', url);
return url;
});
}
@@ -2451,17 +2550,8 @@
},
probeSystemFeatures: function() {
- var sessionid = classes.rpc.getSessionID();
-
- if (sysFeatures == null) {
- try {
- var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
-
- if (this.isObject(data) && this.isObject(data[sessionid]))
- sysFeatures = data[sessionid];
- }
- catch (e) {}
- }
+ if (sysFeatures == null)
+ sysFeatures = Session.getLocalData('features');
if (!this.isObject(sysFeatures)) {
sysFeatures = classes.rpc.declare({
@@ -2469,14 +2559,7 @@
method: 'getFeatures',
expect: { '': {} }
})().then(function(features) {
- try {
- var data = {};
- data[sessionid] = features;
-
- window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
- }
- catch (e) {}
-
+ Session.setLocalData('features', features);
sysFeatures = features;
return features;
@@ -2486,6 +2569,39 @@
return Promise.resolve(sysFeatures);
},
+ probePreloadClasses: function() {
+ if (preloadClasses == null)
+ preloadClasses = Session.getLocalData('preload');
+
+ if (!Array.isArray(preloadClasses)) {
+ preloadClasses = this.resolveDefault(classes.rpc.declare({
+ object: 'file',
+ method: 'list',
+ params: [ 'path' ],
+ expect: { 'entries': [] }
+ })(this.fspath(this.resource('preload'))), []).then(function(entries) {
+ var classes = [];
+
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type != 'file')
+ continue;
+
+ var m = entries[i].name.match(/(.+)\.js$/);
+
+ if (m)
+ classes.push('preload.%s'.format(m[1]));
+ }
+
+ Session.setLocalData('preload', classes);
+ preloadClasses = classes;
+
+ return classes;
+ });
+ }
+
+ return Promise.resolve(preloadClasses);
+ },
+
/**
* Test whether a particular system feature is available, such as
* hostapd SAE support or an installed firewall. The features are
@@ -2524,7 +2640,7 @@
notifySessionExpiry: function() {
Poll.stop();
- L.ui.showModal(_('Session expired'), [
+ classes.ui.showModal(_('Session expired'), [
E('div', { class: 'alert-message warning' },
_('A new login is required since the authentication session expired.')),
E('div', { class: 'right' },
@@ -2537,7 +2653,7 @@
}, _('To login…')))
]);
- L.raise('SessionError', 'Login session is expired');
+ LuCI.prototype.raise('SessionError', 'Login session is expired');
},
/* private */
@@ -2551,10 +2667,13 @@
rpcClass.setBaseURL(rpcBaseURL);
rpcClass.addInterceptor(function(msg, req) {
- if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
+ if (!LuCI.prototype.isObject(msg) ||
+ !LuCI.prototype.isObject(msg.error) ||
+ msg.error.code != -32002)
return;
- if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+ if (!LuCI.prototype.isObject(req) ||
+ (req.object == 'session' && req.method == 'access'))
return;
return rpcClass.declare({
@@ -2562,7 +2681,7 @@
'method': 'access',
'params': [ 'scope', 'object', 'function' ],
'expect': { access: true }
- })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
+ })('uci', 'luci', 'read').catch(LuCI.prototype.notifySessionExpiry);
});
Request.addInterceptor(function(res) {
@@ -2574,10 +2693,31 @@
if (!isDenied)
return;
- L.notifySessionExpiry();
+ LuCI.prototype.notifySessionExpiry();
});
- return this.probeSystemFeatures().finally(this.initDOM);
+ document.addEventListener('poll-start', function(ev) {
+ uiClass.showIndicator('poll-status', _('Refreshing'), function(ev) {
+ Request.poll.active() ? Request.poll.stop() : Request.poll.start();
+ });
+ });
+
+ document.addEventListener('poll-stop', function(ev) {
+ uiClass.showIndicator('poll-status', _('Paused'), null, 'inactive');
+ });
+
+ return Promise.all([
+ this.probeSystemFeatures(),
+ this.probePreloadClasses()
+ ]).finally(LuCI.prototype.bind(function() {
+ var tasks = [];
+
+ if (Array.isArray(preloadClasses))
+ for (var i = 0; i < preloadClasses.length; i++)
+ tasks.push(this.require(preloadClasses[i]));
+
+ return Promise.all(tasks);
+ }, this)).finally(this.initDOM);
},
/* private */
@@ -2594,7 +2734,38 @@
* @instance
* @memberof LuCI
*/
- env: {},
+ env: env,
+
+ /**
+ * Construct an absolute filesystem path relative to the server
+ * document root.
+ *
+ * @instance
+ * @memberof LuCI
+ *
+ * @param {...string} [parts]
+ * An array of parts to join into a path.
+ *
+ * @return {string}
+ * Return the joined path.
+ */
+ fspath: function(/* ... */) {
+ var path = env.documentroot;
+
+ for (var i = 0; i < arguments.length; i++)
+ path += '/' + arguments[i];
+
+ var p = path.replace(/\/+$/, '').replace(/\/+/g, '/').split(/\//),
+ res = [];
+
+ for (var i = 0; i < p.length; i++)
+ if (p[i] == '..')
+ res.pop();
+ else if (p[i] != '.')
+ res.push(p[i]);
+
+ return res.join('/');
+ },
/**
* Construct a relative URL path from the given prefix and parts.
@@ -2648,7 +2819,7 @@
* Returns the resulting URL path.
*/
url: function() {
- return this.path(this.env.scriptname, arguments);
+ return this.path(env.scriptname, arguments);
},
/**
@@ -2670,7 +2841,7 @@
* Returns the resulting URL path.
*/
resource: function() {
- return this.path(this.env.resource, arguments);
+ return this.path(env.resource, arguments);
},
/**
@@ -2692,7 +2863,7 @@
* Returns the resulting URL path.
*/
media: function() {
- return this.path(this.env.media, arguments);
+ return this.path(env.media, arguments);
},
/**
@@ -2705,7 +2876,7 @@
* Returns the URL path to the current view.
*/
location: function() {
- return this.path(this.env.scriptname, this.env.requestpath);
+ return this.path(env.scriptname, env.requestpath);
},
@@ -2949,9 +3120,9 @@
*/
poll: function(interval, url, args, cb, post) {
if (interval !== null && interval <= 0)
- interval = this.env.pollinterval;
+ interval = env.pollinterval;
- var data = post ? { token: this.env.token } : null,
+ var data = post ? { token: env.token } : null,
method = post ? 'POST' : 'GET';
if (!/^(?:\/|\S+:\/\/)/.test(url))
@@ -2973,6 +3144,22 @@
},
/**
+ * Check whether a view has sufficient permissions.
+ *
+ * @return {boolean|null}
+ * Returns `null` if the current session has no permission at all to
+ * load resources required by the view. Returns `false` if readonly
+ * permissions are granted or `true` if at least one required ACL
+ * group is granted with write permissions.
+ */
+ hasViewPermission: function() {
+ if (!this.isObject(env.nodespec) || !env.nodespec.satisfied)
+ return null;
+
+ return !env.nodespec.readonly;
+ },
+
+ /**
* Deprecated wrapper around {@link LuCI.poll.remove Poll.remove()}.
*
* @deprecated
@@ -3115,7 +3302,7 @@
*/
get: function(url, data, callback, timeout) {
this.active = true;
- L.get(url, data, this._response.bind(this, callback), timeout);
+ LuCI.prototype.get(url, data, this._response.bind(this, callback), timeout);
},
/**
@@ -3142,7 +3329,7 @@
*/
post: function(url, data, callback, timeout) {
this.active = true;
- L.post(url, data, this._response.bind(this, callback), timeout);
+ LuCI.prototype.post(url, data, this._response.bind(this, callback), timeout);
},
/**
@@ -3196,12 +3383,12 @@
* Throws an `InternalError` with the message `Not implemented`
* when invoked.
*/
- send_form: function() { L.error('InternalError', 'Not implemented') },
+ send_form: function() { LuCI.prototype.error('InternalError', 'Not implemented') },
});
- XHR.get = function() { return window.L.get.apply(window.L, arguments) };
- XHR.post = function() { return window.L.post.apply(window.L, arguments) };
- XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
+ XHR.get = function() { return LuCI.prototype.get.apply(LuCI.prototype, arguments) };
+ XHR.post = function() { return LuCI.prototype.post.apply(LuCI.prototype, arguments) };
+ XHR.poll = function() { return LuCI.prototype.poll.apply(LuCI.prototype, arguments) };
XHR.stop = Request.poll.remove.bind(Request.poll);
XHR.halt = Request.poll.stop.bind(Request.poll);
XHR.run = Request.poll.start.bind(Request.poll);
diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js
index bca67849b4..8d825d73a0 100644
--- a/modules/luci-base/htdocs/luci-static/resources/network.js
+++ b/modules/luci-base/htdocs/luci-static/resources/network.js
@@ -356,7 +356,9 @@ function initNetworkState(refresh) {
L.resolveDefault(callLuciWirelessDevices(), {}),
L.resolveDefault(callLuciHostHints(), {}),
getProtocolHandlers(),
- uci.load(['network', 'wireless', 'luci'])
+ L.resolveDefault(uci.load('network')),
+ L.resolveDefault(uci.load('wireless')),
+ L.resolveDefault(uci.load('luci'))
]).then(function(data) {
var netifd_ifaces = data[0],
board_json = data[1],
diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js
index 20b77c18fc..7bfc913367 100644
--- a/modules/luci-base/htdocs/luci-static/resources/rpc.js
+++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js
@@ -93,6 +93,10 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
ret = msg.result;
}
else if (Array.isArray(msg.result)) {
+ if (req.raise && msg.result[0] !== 0)
+ L.raise('RPCError', 'RPC call to %s/%s failed with ubus code %d: %s',
+ req.object, req.method, msg.result[0], this.getStatusText(msg.result[0]));
+
ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
}
@@ -228,6 +232,10 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
* Specfies an optional filter function which is invoked to transform the
* received reply data before it is returned to the caller.
*
+ * @property {boolean} [reject=false]
+ * If set to `true`, non-zero ubus call status codes are treated as fatal
+ * error and lead to the rejection of the call promise. The default
+ * behaviour is to resolve with the call return code value instead.
*/
/**
@@ -316,7 +324,8 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
params: params,
priv: priv,
object: options.object,
- method: options.method
+ method: options.method,
+ raise: options.reject
};
/* build message object */
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 9cc3e26ed2..7f724a17e4 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -134,6 +134,7 @@ var CBIZoneSelect = form.ListValue.extend({
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
@@ -388,6 +389,7 @@ var CBINetworkSelect = form.ListValue.extend({
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
@@ -555,6 +557,7 @@ var CBIDeviceSelect = form.ListValue.extend({
sort: order,
multiple: this.multiple,
optional: this.optional || this.rmempty,
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js
index f381e0b649..e6582b3e2c 100644
--- a/modules/luci-base/htdocs/luci-static/resources/uci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/uci.js
@@ -31,44 +31,50 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
object: 'uci',
method: 'get',
params: [ 'config' ],
- expect: { values: { } }
+ expect: { values: { } },
+ reject: true
}),
-
callOrder: rpc.declare({
object: 'uci',
method: 'order',
- params: [ 'config', 'sections' ]
+ params: [ 'config', 'sections' ],
+ reject: true
}),
callAdd: rpc.declare({
object: 'uci',
method: 'add',
params: [ 'config', 'type', 'name', 'values' ],
- expect: { section: '' }
+ expect: { section: '' },
+ reject: true
}),
callSet: rpc.declare({
object: 'uci',
method: 'set',
- params: [ 'config', 'section', 'values' ]
+ params: [ 'config', 'section', 'values' ],
+ reject: true
}),
callDelete: rpc.declare({
object: 'uci',
method: 'delete',
- params: [ 'config', 'section', 'options' ]
+ params: [ 'config', 'section', 'options' ],
+ reject: true
}),
callApply: rpc.declare({
object: 'uci',
method: 'apply',
- params: [ 'timeout', 'rollback' ]
+ params: [ 'timeout', 'rollback' ],
+ reject: true
}),
callConfirm: rpc.declare({
object: 'uci',
- method: 'confirm'
+ method: 'confirm',
+ reject: true
}),
@@ -547,9 +553,13 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
c[conf][sid] = {};
/* undelete option */
- if (d[conf] && d[conf][sid])
+ if (d[conf] && d[conf][sid]) {
d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });
+ if (d[conf][sid].length == 0)
+ delete d[conf][sid];
+ }
+
c[conf][sid][opt] = val;
}
else {
@@ -784,22 +794,22 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
if (n)
for (var conf in n) {
for (var sid in n[conf]) {
- var r = {
+ var p = {
config: conf,
values: { }
};
for (var k in n[conf][sid]) {
if (k == '.type')
- r.type = n[conf][sid][k];
+ p.type = n[conf][sid][k];
else if (k == '.create')
- r.name = n[conf][sid][k];
+ p.name = n[conf][sid][k];
else if (k.charAt(0) != '.')
- r.values[k] = n[conf][sid][k];
+ p.values[k] = n[conf][sid][k];
}
snew.push(n[conf][sid]);
- tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
+ tasks.push(self.callAdd(p.config, p.type, p.name, p.values));
}
pkgs[conf] = true;
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index 61ae69f1cb..4219932b9a 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -2,6 +2,7 @@
'require validation';
'require baseclass';
'require request';
+'require session';
'require poll';
'require dom';
'require rpc';
@@ -59,6 +60,11 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */
* standard validation constraints are checked. The function should return
* `true` to accept the given input value. Any other return value type is
* converted to a string and treated as validation error message.
+ *
+ * @property {boolean} [disabled=false]
+ * Specifies whether the widget should be rendered in disabled state
+ * (`true`) or not (`false`). Disabled widgets cannot be interacted with
+ * and are displayed in a slightly faded style.
*/
/**
@@ -322,6 +328,7 @@ var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
'type': this.options.password ? 'password' : 'text',
'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
'readonly': this.options.readonly ? '' : null,
+ 'disabled': this.options.disabled ? '' : null,
'maxlength': this.options.maxlength,
'placeholder': this.options.placeholder,
'value': this.value,
@@ -445,6 +452,7 @@ var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
'name': this.options.name,
'class': 'cbi-input-textarea',
'readonly': this.options.readonly ? '' : null,
+ 'disabled': this.options.disabled ? '' : null,
'placeholder': this.options.placeholder,
'style': !this.options.cols ? 'width:100%' : null,
'cols': this.options.cols,
@@ -557,6 +565,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
'type': 'checkbox',
'value': this.options.value_enabled,
'checked': (this.value == this.options.value_enabled) ? '' : null,
+ 'disabled': this.options.disabled ? '' : null,
'data-widget-id': this.options.id ? 'widget.' + this.options.id : null
}));
@@ -708,7 +717,8 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
'name': this.options.name,
'size': this.options.size,
'class': 'cbi-input-select',
- 'multiple': this.options.multiple ? '' : null
+ 'multiple': this.options.multiple ? '' : null,
+ 'disabled': this.options.disabled ? '' : null
}));
if (this.options.optional)
@@ -738,7 +748,8 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
'type': this.options.multiple ? 'checkbox' : 'radio',
'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
'value': keys[i],
- 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null,
+ 'disabled': this.options.disabled ? '' : null
}),
this.choices[keys[i]] || keys[i]
]));
@@ -963,6 +974,7 @@ 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
}, E('ul'));
var keys = Object.keys(this.choices);
@@ -2114,7 +2126,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
render: function() {
var dl = E('div', {
'id': this.options.id,
- 'class': 'cbi-dynlist'
+ 'class': 'cbi-dynlist',
+ 'disabled': this.options.disabled ? '' : null
}, E('div', { 'class': 'add-item' }));
if (this.choices) {
@@ -2130,7 +2143,8 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
'id': this.options.id ? 'widget.' + this.options.id : null,
'type': 'text',
'class': 'cbi-input-text',
- 'placeholder': this.options.placeholder
+ 'placeholder': this.options.placeholder,
+ 'disabled': this.options.disabled ? '' : null
});
dl.lastElementChild.appendChild(inputEl);
@@ -2240,6 +2254,9 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
var dl = ev.currentTarget,
item = findParent(ev.target, '.item');
+ if (this.options.disabled)
+ return;
+
if (item) {
this.removeItem(dl, item);
}
@@ -2562,7 +2579,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
return this.bind(E('div', { 'id': this.options.id }, [
E('button', {
'class': 'btn',
- 'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser')
+ 'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
+ 'disabled': this.options.disabled ? '' : null
}, label),
E('div', {
'class': 'cbi-filebrowser'
@@ -2926,6 +2944,113 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
}
});
+
+function scrubMenu(node) {
+ var hasSatisfiedChild = false;
+
+ if (L.isObject(node.children)) {
+ for (var k in node.children) {
+ var child = scrubMenu(node.children[k]);
+
+ if (child.title)
+ hasSatisfiedChild = hasSatisfiedChild || child.satisfied;
+ }
+ }
+
+ if (L.isObject(node.action) &&
+ node.action.type == 'firstchild' &&
+ hasSatisfiedChild == false)
+ node.satisfied = false;
+
+ return node;
+};
+
+/**
+ * Handle menu.
+ *
+ * @constructor menu
+ * @memberof LuCI.ui
+ *
+ * @classdesc
+ *
+ * Handles menus.
+ */
+var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
+ /**
+ * @typedef {Object} MenuNode
+ * @memberof LuCI.ui.menu
+
+ * @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 {readonly} [boolean] - Boolean indicating whether the menu entries underlying ACLs are readonly
+ * @property {LuCI.ui.menu.MenuNode[]} [children] - Array of child menu nodes.
+ */
+
+ /**
+ * Load and cache current menu tree.
+ *
+ * @returns {Promise<LuCI.ui.menu.MenuNode>}
+ * Returns a promise resolving to the root element of the menu tree.
+ */
+ load: function() {
+ if (this.menu == null)
+ this.menu = session.getLocalData('menu');
+
+ if (!L.isObject(this.menu)) {
+ this.menu = request.get(L.url('admin/menu')).then(L.bind(function(menu) {
+ this.menu = scrubMenu(menu.json());
+ session.setLocalData('menu', this.menu);
+
+ return this.menu;
+ }, this));
+ }
+
+ return Promise.resolve(this.menu);
+ },
+
+ /**
+ * Flush the internal menu cache to force loading a new structure on the
+ * next page load.
+ */
+ flushCache: function() {
+ session.setLocalData('menu', null);
+ },
+
+ /**
+ * @param {LuCI.ui.menu.MenuNode} [node]
+ * The menu node to retrieve the children for. Defaults to the menu's
+ * internal root node if omitted.
+ *
+ * @returns {LuCI.ui.menu.MenuNode[]}
+ * Returns an array of child menu nodes.
+ */
+ getChildren: function(node) {
+ var children = [];
+
+ if (node == null)
+ node = this.menu;
+
+ for (var k in node.children) {
+ if (!node.children.hasOwnProperty(k))
+ continue;
+
+ if (!node.children[k].satisfied)
+ continue;
+
+ if (!node.children[k].hasOwnProperty('title'))
+ continue;
+
+ children.push(Object.assign(node.children[k], { name: k }));
+ }
+
+ return children.sort(function(a, b) {
+ return ((a.order || 1000) - (b.order || 1000));
+ });
+ }
+});
+
/**
* @class ui
* @memberof LuCI
@@ -3187,12 +3312,23 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
}
var handlerFn = (typeof(handler) == 'function') ? handler : null,
- indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) ||
- indicatorDiv.appendChild(E('span', {
+ indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id));
+
+ if (indicatorElem == null) {
+ var beforeElem = null;
+
+ for (beforeElem = indicatorDiv.firstElementChild;
+ beforeElem != null;
+ beforeElem = beforeElem.nextElementSibling)
+ if (beforeElem.getAttribute('data-indicator') > id)
+ break;
+
+ indicatorElem = indicatorDiv.insertBefore(E('span', {
'data-indicator': id,
'data-clickable': handlerFn ? true : null,
'click': handlerFn
- }, ['']));
+ }, ['']), beforeElem);
+ }
if (label == indicatorElem.firstChild.data && style == indicatorElem.getAttribute('data-style'))
return false;
@@ -3447,16 +3583,14 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
/** @private */
getActiveTabState: function() {
- var page = document.body.getAttribute('data-page');
+ var page = document.body.getAttribute('data-page'),
+ state = session.getLocalData('tab');
- try {
- var val = JSON.parse(window.sessionStorage.getItem('tab'));
- if (val.page === page && L.isObject(val.paths))
- return val;
- }
- catch(e) {}
+ if (L.isObject(state) && state.page === page && L.isObject(state.paths))
+ return state;
+
+ session.setLocalData('tab', null);
- window.sessionStorage.removeItem('tab');
return { page: page, paths: {} };
},
@@ -3468,17 +3602,12 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
/** @private */
setActiveTabId: function(pane, tabIndex) {
- var path = this.getPathForPane(pane);
+ var path = this.getPathForPane(pane),
+ state = this.getActiveTabState();
- try {
- var state = this.getActiveTabState();
- state.paths[path] = tabIndex;
-
- window.sessionStorage.setItem('tab', JSON.stringify(state));
- }
- catch (e) { return false; }
+ state.paths[path] = tabIndex;
- return true;
+ return session.setLocalData('tab', state);
},
/** @private */
@@ -3784,26 +3913,13 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
* The number of changes to indicate.
*/
setIndicator: function(n) {
- var i = document.querySelector('.uci_change_indicator');
- if (i == null) {
- var poll = document.getElementById('xhr_poll_status');
- i = poll.parentNode.insertBefore(E('a', {
- 'href': '#',
- 'class': 'uci_change_indicator label notice',
- 'click': L.bind(this.displayChanges, this)
- }), poll);
- }
-
if (n > 0) {
- dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
- i.classList.add('flash');
- i.style.display = '';
- document.dispatchEvent(new CustomEvent('uci-new-changes'));
+ UI.prototype.showIndicator('uci-changes',
+ '%s: %d'.format(_('Unsaved Changes'), n),
+ L.bind(this.displayChanges, this));
}
else {
- i.classList.remove('flash');
- i.style.display = 'none';
- document.dispatchEvent(new CustomEvent('uci-clear-changes'));
+ UI.prototype.hideIndicator('uci-changes');
}
},
@@ -4300,6 +4416,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
});
},
+ menu: UIMenu,
+
AbstractElement: UIElement,
/* Widgets */