diff options
Diffstat (limited to 'modules/luci-base/htdocs/luci-static')
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 */ |