summaryrefslogtreecommitdiffhomepage
path: root/modules
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2019-04-01 16:22:13 +0200
committerJo-Philipp Wich <jo@mein.io>2019-07-07 15:36:24 +0200
commitb839ee87f2df52d5445bd668352adbca6d2c906b (patch)
tree62f928e0bb23ced2269df44833530888359e4f29 /modules
parent9c7eb1decd82344e22a10e6f5ac36b463d2149f5 (diff)
luci-base: introduce form.js
Add a new client side form.js library which is a more or less direct reimplementation of the Lua side cbi.lua in JavaScript. Due to its client side nature, it supports a number of features which would be hard to realize on the server side, such as drag&drop sorting, modal sub-map dialogs, reload-free editing etc. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js1669
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js3
2 files changed, 1671 insertions, 1 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
new file mode 100644
index 000000000..835c1fbb6
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -0,0 +1,1669 @@
+'use strict';
+'require ui';
+'require uci';
+
+var scope = this;
+
+var CBINode = Class.extend({
+ __init__: function(title, description) {
+ this.title = title || '';
+ this.description = description || '';
+ this.children = [];
+ },
+
+ append: function(obj) {
+ this.children.push(obj);
+ },
+
+ parse: function() {
+ var args = arguments;
+ this.children.forEach(function(child) {
+ child.parse.apply(child, args);
+ });
+ },
+
+ render: function() {
+ L.error('InternalError', 'Not implemented');
+ },
+
+ loadChildren: function(/* ... */) {
+ var tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ if (!this.children[i].disable)
+ tasks.push(this.children[i].load.apply(this.children[i], arguments));
+
+ return Promise.all(tasks);
+ },
+
+ renderChildren: function(tab_name /*, ... */) {
+ var tasks = [],
+ index = 0;
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ if (tab_name === null || this.children[i].tab === tab_name)
+ if (!this.children[i].disable)
+ tasks.push(this.children[i].render.apply(
+ this.children[i], this.varargs(arguments, 1, index++)));
+
+ return Promise.all(tasks);
+ },
+
+ stripTags: function(s) {
+ if (!s.match(/[<>]/))
+ return s;
+
+ var x = E('div', {}, s);
+ return x.textContent || x.innerText || '';
+ }
+});
+
+var CBIMap = CBINode.extend({
+ __init__: function(config /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 1));
+
+ this.config = config;
+ this.parsechain = [ config ];
+ },
+
+ findElements: function(/* ... */) {
+ var q = null;
+
+ if (arguments.length == 1)
+ q = arguments[0];
+ else if (arguments.length == 2)
+ q = '[%s="%s"]'.format(arguments[0], arguments[1]);
+ else
+ L.error('InternalError', 'Expecting one or two arguments to findElements()');
+
+ return this.root.querySelectorAll(q);
+ },
+
+ findElement: function(/* ... */) {
+ var res = this.findElements.apply(this, arguments);
+ return res.length ? res[0] : null;
+ },
+
+ chain: function(config) {
+ if (this.parsechain.indexOf(config) == -1)
+ this.parsechain.push(config);
+ },
+
+ section: function(cbiClass /*, ... */) {
+ if (!CBIAbstractSection.isSubclass(cbiClass))
+ L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
+
+ var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
+ this.append(obj);
+ return obj;
+ },
+
+ load: function() {
+ return uci.load(this.parsechain || [ this.config ])
+ .then(this.loadChildren.bind(this));
+ },
+
+ parse: function() {
+ var tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ tasks.push(this.children[i].parse());
+
+ return Promise.all(tasks);
+ },
+
+ save: function() {
+ return this.parse()
+ .then(uci.save.bind(uci))
+ .then(this.load.bind(this))
+ .then(this.renderContents.bind(this))
+ .catch(function() { alert('Cannot save due to invalid values') });
+ },
+
+ reset: function() {
+ return this.renderContents();
+ },
+
+ render: function() {
+ return this.load().then(this.renderContents.bind(this));
+ },
+
+ renderContents: function() {
+ var mapEl = this.root || (this.root = E('div', {
+ 'id': 'cbi-%s'.format(this.config),
+ 'class': 'cbi-map',
+ 'cbi-dependency-check': L.bind(this.checkDepends, this)
+ }));
+
+ L.dom.bindClassInstance(mapEl, this);
+
+ return this.renderChildren(null).then(L.bind(function(nodes) {
+ var initialRender = !mapEl.firstChild;
+
+ L.dom.content(mapEl, null);
+
+ if (this.title != null && this.title != '')
+ mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
+
+ if (this.description != null && this.description != '')
+ mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
+
+ L.dom.append(mapEl, nodes);
+
+ if (!initialRender) {
+ mapEl.classList.remove('flash');
+
+ window.setTimeout(function() {
+ mapEl.classList.add('flash');
+ }, 1);
+ }
+
+ this.checkDepends();
+
+ return mapEl;
+ }, this));
+ },
+
+ lookupOption: function(name, section_id) {
+ var id, elem, sid, inst;
+
+ if (name.indexOf('.') > -1)
+ id = 'cbid.%s'.format(name);
+ else
+ id = 'cbid.%s.%s.%s'.format(this.config, section_id, name);
+
+ elem = this.findElement('data-field', id);
+ sid = elem ? id.split(/\./)[2] : null;
+ inst = elem ? L.dom.findClassInstance(elem) : null;
+
+ return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
+ },
+
+ checkDepends: function(ev, n) {
+ var changed = false;
+
+ for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
+ if (s.checkDepends(ev, n))
+ changed = true;
+
+ if (changed && (n || 0) < 10)
+ this.checkDepends(ev, (n || 10) + 1);
+ }
+});
+
+var CBIAbstractSection = CBINode.extend({
+ __init__: function(map, sectionType /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 2));
+
+ this.sectiontype = sectionType;
+ this.map = map;
+ this.config = map.config;
+
+ this.optional = true;
+ this.addremove = false;
+ this.dynamic = false;
+ },
+
+ cfgsections: function() {
+ L.error('InternalError', 'Not implemented');
+ },
+
+ filter: function(section_id) {
+ return true;
+ },
+
+ load: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < section_ids.length; i++)
+ tasks.push(this.loadChildren(section_ids[i])
+ .then(Function.prototype.bind.call(function(section_id, set_values) {
+ for (var i = 0; i < set_values.length; i++)
+ this.children[i].cfgvalue(section_id, set_values[i]);
+ }, this, section_ids[i])));
+
+ return Promise.all(tasks);
+ },
+
+ parse: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < section_ids.length; i++)
+ for (var j = 0; j < this.children.length; j++)
+ tasks.push(this.children[j].parse(section_ids[i]));
+
+ return Promise.all(tasks);
+ },
+
+ tab: function(name, title, description) {
+ if (this.tabs && this.tabs[name])
+ throw 'Tab already declared';
+
+ var entry = {
+ name: name,
+ title: title,
+ description: description,
+ children: []
+ };
+
+ this.tabs = this.tabs || [];
+ this.tabs.push(entry);
+ this.tabs[name] = entry;
+
+ this.tab_names = this.tab_names || [];
+ this.tab_names.push(name);
+ },
+
+ option: function(cbiClass /*, ... */) {
+ if (!CBIAbstractValue.isSubclass(cbiClass))
+ throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
+
+ var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
+ this.append(obj);
+ return obj;
+ },
+
+ taboption: function(tabName /*, ... */) {
+ if (!this.tabs || !this.tabs[tabName])
+ throw L.error('ReferenceError', 'Associated tab not declared');
+
+ var obj = this.option.apply(this, this.varargs(arguments, 1));
+ obj.tab = tabName;
+ this.tabs[tabName].children.push(obj);
+ return obj;
+ },
+
+ renderUCISection: function(section_id) {
+ var renderTasks = [];
+
+ if (!this.tabs)
+ return this.renderOptions(null, section_id);
+
+ for (var i = 0; i < this.tab_names.length; i++)
+ renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
+
+ return Promise.all(renderTasks)
+ .then(this.renderTabContainers.bind(this, section_id));
+ },
+
+ renderTabContainers: function(section_id, nodes) {
+ var config_name = this.uciconfig || this.map.config,
+ containerEls = E([]);
+
+ for (var i = 0; i < nodes.length; i++) {
+ var tab_name = this.tab_names[i],
+ tab_data = this.tabs[tab_name],
+ containerEl = E('div', {
+ 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
+ 'data-tab': tab_name,
+ 'data-tab-title': tab_data.title,
+ 'data-tab-active': tab_name === this.selected_tab
+ });
+
+ if (tab_data.description != null && tab_data.description != '')
+ containerEl.appendChild(
+ E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
+
+ containerEl.appendChild(nodes[i]);
+ containerEls.appendChild(containerEl);
+ }
+
+ return containerEls;
+ },
+
+ renderOptions: function(tab_name, section_id) {
+ var in_table = (this instanceof CBITableSection);
+ return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
+ var optionEls = E([]);
+ for (var i = 0; i < nodes.length; i++)
+ optionEls.appendChild(nodes[i]);
+ return optionEls;
+ });
+ },
+
+ checkDepends: function(ev, n) {
+ var changed = false,
+ sids = this.cfgsections();
+
+ for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
+ for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
+ var isActive = o.isActive(sid),
+ isSatisified = o.checkDepends(sid);
+
+ if (isActive != isSatisified) {
+ o.setActive(sid, !isActive);
+ changed = true;
+ }
+
+ if (!n && isActive)
+ o.triggerValidation(sid);
+ }
+ }
+
+ return changed;
+ }
+});
+
+
+var isEqual = function(x, y) {
+ if (x != null && y != null && typeof(x) != typeof(y))
+ return false;
+
+ if ((x == null && y != null) || (x != null && y == null))
+ return false;
+
+ if (Array.isArray(x)) {
+ if (x.length != y.length)
+ return false;
+
+ for (var i = 0; i < x.length; i++)
+ if (!isEqual(x[i], y[i]))
+ return false;
+ }
+ else if (typeof(x) == 'object') {
+ for (var k in x) {
+ if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
+ return false;
+
+ if (!isEqual(x[k], y[k]))
+ return false;
+ }
+
+ for (var k in y)
+ if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
+ return false;
+ }
+ else if (x != y) {
+ return false;
+ }
+
+ return true;
+};
+
+var CBIAbstractValue = CBINode.extend({
+ __init__: function(map, section, option /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 3));
+
+ this.section = section;
+ this.option = option;
+ this.map = map;
+ this.config = map.config;
+
+ this.deps = [];
+ this.initial = {};
+ this.rmempty = true;
+ this.default = null;
+ this.size = null;
+ this.optional = false;
+ },
+
+ depends: function(field, value) {
+ var deps;
+
+ if (typeof(field) === 'string')
+ deps = {}, deps[field] = value;
+ else
+ deps = field;
+
+ this.deps.push(deps);
+ },
+
+ transformDepList: function(section_id, deplist) {
+ var list = deplist || this.deps,
+ deps = [];
+
+ if (Array.isArray(list)) {
+ for (var i = 0; i < list.length; i++) {
+ var dep = {};
+
+ for (var k in list[i]) {
+ if (list[i].hasOwnProperty(k)) {
+ if (k.charAt(0) === '!')
+ dep[k] = list[i][k];
+ else if (k.indexOf('.') !== -1)
+ dep['cbid.%s'.format(k)] = list[i][k];
+ else
+ dep['cbid.%s.%s.%s'.format(this.config, this.ucisection || section_id, k)] = list[i][k];
+ }
+ }
+
+ for (var k in dep) {
+ if (dep.hasOwnProperty(k)) {
+ deps.push(dep);
+ break;
+ }
+ }
+ }
+ }
+
+ return deps;
+ },
+
+ transformChoices: function() {
+ if (!Array.isArray(this.keylist) || this.keylist.length == 0)
+ return null;
+
+ var choices = {};
+
+ for (var i = 0; i < this.keylist.length; i++)
+ choices[this.keylist[i]] = this.vallist[i];
+
+ return choices;
+ },
+
+ checkDepends: function(section_id) {
+ var def = false;
+
+ if (!Array.isArray(this.deps) || !this.deps.length)
+ return true;
+
+ for (var i = 0; i < this.deps.length; i++) {
+ var istat = true,
+ reverse = false;
+
+ for (var dep in this.deps[i]) {
+ if (dep == '!reverse') {
+ reverse = true;
+ }
+ else if (dep == '!default') {
+ def = true;
+ istat = false;
+ }
+ else {
+ var res = this.map.lookupOption(dep, section_id),
+ val = res ? res[0].formvalue(res[1]) : null;
+
+ istat = (istat && isEqual(val, this.deps[i][dep]));
+ }
+ }
+
+ if (istat ^ reverse)
+ return true;
+ }
+
+ return def;
+ },
+
+ cbid: function(section_id) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ return 'cbid.%s.%s.%s'.format(this.map.config, section_id, this.option);
+ },
+
+ load: function(section_id) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ return uci.get(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option);
+ },
+
+ cfgvalue: function(section_id, set_value) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ if (arguments.length == 2) {
+ this.data = this.data || {};
+ this.data[section_id] = set_value;
+ }
+
+ return this.data ? this.data[section_id] : null;
+ },
+
+ formvalue: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'getValue') : null;
+ },
+
+ textvalue: function(section_id) {
+ var cval = this.cfgvalue(section_id);
+
+ if (cval == null)
+ cval = this.default;
+
+ return (cval != null) ? '%h'.format(cval) : null;
+ },
+
+ validate: function(section_id, value) {
+ return true;
+ },
+
+ isValid: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'isValid') : true;
+ },
+
+ isActive: function(section_id) {
+ var field = this.map.findElement('data-field', this.cbid(section_id));
+ return (field != null && !field.classList.contains('hidden'));
+ },
+
+ setActive: function(section_id, active) {
+ var field = this.map.findElement('data-field', this.cbid(section_id));
+
+ if (field && field.classList.contains('hidden') == active) {
+ field.classList[active ? 'remove' : 'add']('hidden');
+ return true;
+ }
+
+ return false;
+ },
+
+ triggerValidation: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
+ },
+
+ parse: function(section_id) {
+ var active = this.isActive(section_id),
+ cval = this.cfgvalue(section_id),
+ fval = active ? this.formvalue(section_id) : null;
+
+ if (active && !this.isValid(section_id))
+ return Promise.reject();
+
+ if (fval != '' && fval != null) {
+ if (this.forcewrite || !isEqual(cval, fval))
+ return Promise.resolve(this.write(section_id, fval));
+ }
+ else {
+ if (this.rmempty || this.optional) {
+ return Promise.resolve(this.remove(section_id));
+ }
+ else if (!isEqual(cval, fval)) {
+ console.log('This should have been catched by isValid()');
+ return Promise.reject();
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ write: function(section_id, formvalue) {
+ return uci.set(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option,
+ formvalue);
+ },
+
+ remove: function(section_id) {
+ return uci.unset(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option);
+ }
+});
+
+var CBITypedSection = CBIAbstractSection.extend({
+ __name__: 'CBI.TypedSection',
+
+ cfgsections: function() {
+ return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
+ .map(function(s) { return s['.name'] })
+ .filter(L.bind(this.filter, this));
+ },
+
+ handleAdd: function(ev, name) {
+ var config_name = this.uciconfig || this.map.config;
+
+ uci.add(config_name, this.sectiontype, name);
+ this.map.save();
+ },
+
+ handleRemove: function(section_id, ev) {
+ var config_name = this.uciconfig || this.map.config;
+
+ uci.remove(config_name, section_id);
+ this.map.save();
+ },
+
+ renderSectionAdd: function(extra_class) {
+ if (!this.addremove)
+ return E([]);
+
+ var createEl = E('div', { 'class': 'cbi-section-create' }),
+ config_name = this.uciconfig || this.map.config;
+
+ if (extra_class != null)
+ createEl.classList.add(extra_class);
+
+ if (this.anonymous) {
+ createEl.appendChild(E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button cbi-button-add',
+ 'value': _('Add'),
+ 'title': _('Add'),
+ 'click': L.bind(this.handleAdd, this)
+ }));
+ }
+ else {
+ var nameEl = E('input', {
+ 'type': 'text',
+ 'class': 'cbi-section-create-name'
+ });
+
+ L.dom.append(createEl, [
+ E('div', {}, nameEl),
+ E('input', {
+ 'class': 'cbi-button cbi-button-add',
+ 'type': 'submit',
+ 'value': _('Add'),
+ 'title': _('Add'),
+ 'click': L.bind(function(ev) {
+ if (nameEl.classList.contains('cbi-input-invalid'))
+ return;
+
+ this.handleAdd(ev, nameEl.value);
+ }, this)
+ })
+ ]);
+
+ ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
+ }
+
+ return createEl;
+ },
+
+ renderContents: function(cfgsections, nodes) {
+ var section_id = null,
+ config_name = this.uciconfig || this.map.config,
+ sectionEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+ 'class': 'cbi-section'
+ });
+
+ if (this.title != null && this.title != '')
+ sectionEl.appendChild(E('legend', {}, this.title));
+
+ if (this.description != null && this.description != '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ for (var i = 0; i < nodes.length; i++) {
+ if (this.addremove) {
+ sectionEl.appendChild(
+ E('div', { 'class': 'cbi-section-remove right' },
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button',
+ 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
+ 'value': _('Delete'),
+ 'data-section-id': cfgsections[i],
+ 'click': L.bind(this.handleRemove, this, cfgsections[i])
+ })));
+ }
+
+ if (!this.anonymous)
+ sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
+
+ sectionEl.appendChild(E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
+ 'class': this.tabs
+ ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
+ }, nodes[i]));
+
+ if (this.tabs)
+ ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
+ }
+
+ if (nodes.length == 0)
+ L.dom.append(sectionEl, [
+ E('em', _('This section contains no values yet')),
+ E('br'), E('br')
+ ]);
+
+ sectionEl.appendChild(this.renderSectionAdd());
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ render: function() {
+ var cfgsections = this.cfgsections(),
+ renderTasks = [];
+
+ for (var i = 0; i < cfgsections.length; i++)
+ renderTasks.push(this.renderUCISection(cfgsections[i]));
+
+ return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
+ }
+});
+
+var CBITableSection = CBITypedSection.extend({
+ __name__: 'CBI.TableSection',
+
+ tab: function() {
+ throw 'Tabs are not supported by TableSection';
+ },
+
+ renderContents: function(cfgsections, nodes) {
+ var section_id = null,
+ config_name = this.uciconfig || this.map.config,
+ max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
+ has_more = max_cols < this.children.length,
+ sectionEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+ 'class': 'cbi-section cbi-tblsection'
+ }),
+ tableEl = E('div', {
+ 'class': 'table cbi-section-table'
+ });
+
+ if (this.title != null && this.title != '')
+ sectionEl.appendChild(E('h3', {}, this.title));
+
+ if (this.description != null && this.description != '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ tableEl.appendChild(this.renderHeaderRows(max_cols));
+
+ for (var i = 0; i < nodes.length; i++) {
+ var sectionname = this.stripTags((typeof(this.sectiontitle) == 'function')
+ ? String(this.sectiontitle(cfgsections[i]) || '') : cfgsections[i]).trim();
+
+ var trEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
+ 'class': 'tr cbi-section-table-row',
+ 'data-sid': cfgsections[i],
+ 'draggable': this.sortable ? true : null,
+ 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
+ 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
+ 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
+ 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
+ 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
+ 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
+ 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
+ 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null
+ });
+
+ if (this.extedit || this.rowcolors)
+ trEl.classList.add(!(tableEl.childNodes.length % 2)
+ ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
+
+ for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
+ trEl.appendChild(nodes[i].firstChild);
+
+ trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
+ tableEl.appendChild(trEl);
+ }
+
+ if (nodes.length == 0)
+ tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
+ E('div', { 'class': 'td' },
+ E('em', {}, _('This section contains no values yet')))));
+
+ sectionEl.appendChild(tableEl);
+
+ sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ renderHeaderRows: function(max_cols) {
+ var has_titles = false,
+ has_descriptions = false,
+ anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
+ trEls = E([]);
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ has_titles = has_titles || !!opt.title;
+ has_descriptions = has_descriptions || !!opt.description;
+ }
+
+ if (has_titles) {
+ var trEl = E('div', {
+ 'class': 'tr cbi-section-table-titles ' + anon_class,
+ 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
+ });
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell',
+ 'data-type': opt.__name__
+ }));
+
+ if (opt.width != null)
+ trEl.lastElementChild.style.width =
+ (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+
+ if (opt.titleref)
+ trEl.lastElementChild.appendChild(E('a', {
+ 'href': opt.titleref,
+ 'class': 'cbi-title-ref',
+ 'title': this.titledesc || _('Go to relevant configuration page')
+ }, opt.title));
+ else
+ L.dom.content(trEl.lastElementChild, opt.title);
+ }
+
+ if (this.sortable || this.extedit || this.addremove || has_more)
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell cbi-section-actions'
+ }));
+
+ trEls.appendChild(trEl);
+ }
+
+ if (has_descriptions) {
+ var trEl = E('div', {
+ 'class': 'tr cbi-section-table-descr ' + anon_class
+ });
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell',
+ 'data-type': opt.__name__
+ }, opt.description));
+
+ if (opt.width != null)
+ trEl.lastElementChild.style.width =
+ (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+ }
+
+ if (this.sortable || this.extedit || this.addremove || has_more)
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell cbi-section-actions'
+ }));
+
+ trEls.appendChild(trEl);
+ }
+
+ return trEls;
+ },
+
+ renderRowActions: function(section_id, more_label) {
+ var config_name = this.uciconfig || this.map.config;
+
+ if (!this.sortable && !this.extedit && !this.addremove && !more_label)
+ return E([]);
+
+ var tdEl = E('div', {
+ 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
+ }, E('div'));
+
+ if (this.sortable) {
+ L.dom.append(tdEl.lastElementChild, [
+ E('div', {
+ 'title': _('Drag to reorder'),
+ 'class': 'cbi-button drag-handle center',
+ 'style': 'cursor:move'
+ }, '☰')
+ ]);
+ }
+
+ if (this.extedit) {
+ var evFn = null;
+
+ if (typeof(this.extedit) == 'function')
+ evFn = L.bind(this.extedit, this);
+ else if (typeof(this.extedit) == 'string')
+ evFn = L.bind(function(sid, ev) {
+ location.href = this.extedit.format(sid);
+ }, this, section_id);
+
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'button',
+ 'value': _('Edit'),
+ 'title': _('Edit'),
+ 'class': 'cbi-button cbi-button-edit',
+ 'click': evFn
+ })
+ );
+ }
+
+ if (more_label) {
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'button',
+ 'value': more_label,
+ 'title': more_label,
+ 'class': 'cbi-button cbi-button-edit',
+ 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
+ })
+ );
+ }
+
+ if (this.addremove) {
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'submit',
+ 'value': _('Delete'),
+ 'title': _('Delete'),
+ 'class': 'cbi-button cbi-button-remove',
+ 'click': L.bind(function(sid, ev) {
+ uci.remove(config_name, sid);
+ this.map.save();
+ }, this, section_id)
+ })
+ );
+ }
+
+ return tdEl;
+ },
+
+ handleDragInit: function(ev) {
+ scope.dragState = { node: ev.target };
+ },
+
+ handleDragStart: function(ev) {
+ if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
+ scope.dragState = null;
+ ev.preventDefault();
+ return false;
+ }
+
+ scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
+ ev.dataTransfer.setData('text', 'drag');
+ ev.target.style.opacity = 0.4;
+ },
+
+ handleDragOver: function(ev) {
+ var n = scope.dragState.targetNode,
+ r = scope.dragState.rect,
+ t = r.top + r.height / 2;
+
+ if (ev.clientY <= t) {
+ n.classList.remove('drag-over-below');
+ n.classList.add('drag-over-above');
+ }
+ else {
+ n.classList.remove('drag-over-above');
+ n.classList.add('drag-over-below');
+ }
+
+ ev.dataTransfer.dropEffect = 'move';
+ ev.preventDefault();
+ return false;
+ },
+
+ handleDragEnter: function(ev) {
+ scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
+ scope.dragState.targetNode = ev.currentTarget;
+ },
+
+ handleDragLeave: function(ev) {
+ ev.currentTarget.classList.remove('drag-over-above');
+ ev.currentTarget.classList.remove('drag-over-below');
+ },
+
+ handleDragEnd: function(ev) {
+ var n = ev.target;
+
+ n.style.opacity = '';
+ n.classList.add('flash');
+ n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
+ .forEach(function(tr) {
+ tr.classList.remove('drag-over-above');
+ tr.classList.remove('drag-over-below');
+ });
+ },
+
+ handleDrop: function(ev) {
+ var s = scope.dragState;
+
+ if (s.node && s.targetNode) {
+ var config_name = this.uciconfig || this.map.config,
+ ref_node = s.targetNode,
+ after = false;
+
+ if (ref_node.classList.contains('drag-over-below')) {
+ ref_node = ref_node.nextElementSibling;
+ after = true;
+ }
+
+ var sid1 = s.node.getAttribute('data-sid'),
+ sid2 = s.targetNode.getAttribute('data-sid');
+
+ s.node.parentNode.insertBefore(s.node, ref_node);
+ uci.move(config_name, sid1, sid2, after);
+ }
+
+ scope.dragState = null;
+ ev.target.style.opacity = '';
+ ev.stopPropagation();
+ ev.preventDefault();
+ return false;
+ },
+
+ handleModalCancel: function(modalMap, ev) {
+ L.ui.hideModal();
+ },
+
+ handleModalSave: function(modalMap, ev) {
+ modalMap.save()
+ .then(L.bind(this.map.reset, this.map))
+ .then(L.ui.hideModal);
+ },
+
+ renderMoreOptionsModal: function(section_id, ev) {
+ var parent = this.map,
+ title = parent.title,
+ name = null,
+ m = new CBIMap(this.map.config, null, null),
+ s = m.section(CBINamedSection, section_id, this.sectiontype);
+ s.tabs = this.tabs;
+ s.tab_names = this.tab_names;
+
+ if (typeof(this.sectiontitle) == 'function')
+ name = this.stripTags(String(this.sectiontitle(section_id) || ''));
+ else if (!this.anonymous)
+ name = section_id;
+
+ if (name)
+ title += ' - ' + name;
+
+ for (var i = 0; i < this.children.length; i++) {
+ var o1 = this.children[i];
+
+ if (o1.disabled || o1.modalonly === false)
+ continue;
+
+ var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
+
+ for (var k in o1) {
+ if (!o1.hasOwnProperty(k))
+ continue;
+
+ switch (k) {
+ case 'map':
+ case 'section':
+ case 'option':
+ case 'title':
+ case 'description':
+ continue;
+
+ default:
+ o2[k] = o1[k];
+ }
+ }
+ }
+
+ //ev.target.classList.add('spinning');
+ m.render().then(L.bind(function(nodes) {
+ //ev.target.classList.remove('spinning');
+ L.ui.showModal(title, [
+ nodes,
+ E('div', { 'class': 'right' }, [
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn',
+ 'click': L.bind(this.handleModalCancel, this, m),
+ 'value': _('Dismiss')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'cbi-button cbi-button-positive important',
+ 'click': L.bind(this.handleModalSave, this, m),
+ 'value': _('Save')
+ })
+ ])
+ ]);
+ }, this)).catch(L.error);
+ }
+});
+
+var CBIGridSection = CBITableSection.extend({
+ tab: function(name, title, description) {
+ CBIAbstractSection.prototype.tab.call(this, name, title, description);
+ },
+
+ handleAdd: function(ev) {
+ var config_name = this.uciconfig || this.map.config,
+ section_id = uci.add(config_name, this.sectiontype);
+
+ this.addedSection = section_id;
+ this.renderMoreOptionsModal(section_id);
+ },
+
+ handleModalSave: function(/* ... */) {
+ this.super('handleModalSave', arguments);
+ this.addedSection = null;
+ },
+
+ handleModalCancel: function(/* ... */) {
+ var config_name = this.uciconfig || this.map.config;
+
+ if (this.addedSection != null) {
+ uci.remove(config_name, this.addedSection);
+ this.addedSection = null;
+ }
+
+ this.super('handleModalCancel', arguments);
+ },
+
+ renderUCISection: function(section_id) {
+ return this.renderOptions(null, section_id);
+ },
+
+ renderChildren: function(tab_name, section_id, in_table) {
+ var tasks = [], index = 0;
+
+ for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
+ if (opt.disable || opt.modalonly)
+ continue;
+
+ if (opt.editable)
+ tasks.push(opt.render(index++, section_id, in_table));
+ else
+ tasks.push(this.renderTextValue(section_id, opt));
+ }
+
+ return Promise.all(tasks);
+ },
+
+ renderTextValue: function(section_id, opt) {
+ var title = this.stripTags(opt.title).trim(),
+ descr = this.stripTags(opt.description).trim(),
+ value = opt.textvalue(section_id);
+
+ return E('div', {
+ 'class': 'td cbi-value-field',
+ 'data-title': (title != '') ? title : opt.option,
+ 'data-description': (descr != '') ? descr : null,
+ 'data-name': opt.option,
+ 'data-type': opt.typename || opt.__name__
+ }, (value != null) ? value : E('em', _('none')));
+ },
+
+ renderRowActions: function(section_id) {
+ return this.super('renderRowActions', [ section_id, _('Edit') ]);
+ },
+
+ parse: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children)) {
+ for (var i = 0; i < section_ids.length; i++) {
+ for (var j = 0; j < this.children.length; j++) {
+ if (!this.children[j].editable || this.children[j].modalonly)
+ continue;
+
+ tasks.push(this.children[j].parse(section_ids[i]));
+ }
+ }
+ }
+
+ return Promise.all(tasks);
+ }
+});
+
+var CBINamedSection = CBIAbstractSection.extend({
+ __name__: 'CBI.NamedSection',
+ __init__: function(map, section_id /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 2, map));
+
+ this.section = section_id;
+ },
+
+ cfgsections: function() {
+ return [ this.section ];
+ },
+
+ handleAdd: function(ev) {
+ var section_id = this.section,
+ config_name = this.uciconfig || this.map.config;
+
+ uci.add(config_name, this.sectiontype, section_id);
+ this.map.save();
+ },
+
+ handleRemove: function(ev) {
+ var section_id = this.section,
+ config_name = this.uciconfig || this.map.config;
+
+ uci.remove(config_name, section_id);
+ this.map.save();
+ },
+
+ renderContents: function(data) {
+ var ucidata = data[0], nodes = data[1],
+ section_id = this.section,
+ config_name = this.uciconfig || this.map.config,
+ sectionEl = E('div', {
+ 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
+ 'class': 'cbi-section'
+ });
+
+ if (typeof(this.title) === 'string' && this.title !== '')
+ sectionEl.appendChild(E('legend', {}, this.title));
+
+ if (typeof(this.description) === 'string' && this.description !== '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ if (ucidata) {
+ if (this.addremove) {
+ sectionEl.appendChild(
+ E('div', { 'class': 'cbi-section-remove right' },
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button',
+ 'value': _('Delete'),
+ 'click': L.bind(this.handleRemove, this)
+ })));
+ }
+
+ sectionEl.appendChild(E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, section_id),
+ 'class': this.tabs
+ ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
+ }, nodes));
+
+ if (this.tabs)
+ ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
+ }
+ else if (this.addremove) {
+ sectionEl.appendChild(
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button cbi-button-add',
+ 'value': _('Add'),
+ 'click': L.bind(this.handleAdd, this)
+ }));
+ }
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ render: function() {
+ var config_name = this.uciconfig || this.map.config,
+ section_id = this.section;
+
+ return Promise.all([
+ uci.get(config_name, section_id),
+ this.renderUCISection(section_id)
+ ]).then(this.renderContents.bind(this));
+ }
+});
+
+var CBIValue = CBIAbstractValue.extend({
+ __name__: 'CBI.Value',
+
+ value: function(key, val) {
+ this.keylist = this.keylist || [];
+ this.keylist.push(String(key));
+
+ this.vallist = this.vallist || [];
+ this.vallist.push(String(val != null ? val : key));
+ },
+
+ render: function(option_index, section_id, in_table) {
+ return Promise.resolve(this.cfgvalue(section_id))
+ .then(this.renderWidget.bind(this, section_id, option_index))
+ .then(this.renderFrame.bind(this, section_id, in_table, option_index));
+ },
+
+ renderFrame: function(section_id, in_table, option_index, nodes) {
+ var config_name = this.uciconfig || this.map.config,
+ depend_list = this.transformDepList(section_id),
+ optionEl;
+
+ if (in_table) {
+ optionEl = E('div', {
+ 'class': 'td cbi-value-field',
+ 'data-title': this.stripTags(this.title).trim(),
+ 'data-description': this.stripTags(this.description).trim(),
+ 'data-name': this.option,
+ 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
+ }, E('div', {
+ 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
+ 'data-index': option_index,
+ 'data-depends': depend_list,
+ 'data-field': this.cbid(section_id)
+ }));
+ }
+ else {
+ optionEl = E('div', {
+ 'class': 'cbi-value',
+ 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
+ 'data-index': option_index,
+ 'data-depends': depend_list,
+ 'data-field': this.cbid(section_id),
+ 'data-name': this.option,
+ 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
+ });
+
+ if (this.last_child)
+ optionEl.classList.add('cbi-value-last');
+
+ if (typeof(this.title) === 'string' && this.title !== '') {
+ optionEl.appendChild(E('label', {
+ 'class': 'cbi-value-title',
+ 'for': 'cbid.%s.%s.%s'.format(config_name, section_id, this.option)
+ },
+ this.titleref ? E('a', {
+ 'class': 'cbi-title-ref',
+ 'href': this.titleref,
+ 'title': this.titledesc || _('Go to relevant configuration page')
+ }, this.title) : this.title));
+
+ optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
+ }
+ }
+
+ if (nodes)
+ (optionEl.lastChild || optionEl).appendChild(nodes);
+
+ if (!in_table && typeof(this.description) === 'string' && this.description !== '')
+ L.dom.append(optionEl.lastChild || optionEl,
+ E('div', { 'class': 'cbi-value-description' }, this.description));
+
+ if (depend_list && depend_list.length)
+ optionEl.classList.add('hidden');
+
+ optionEl.addEventListener('widget-change',
+ L.bind(this.map.checkDepends, this.map));
+
+ L.dom.bindClassInstance(optionEl, this);
+
+ return optionEl;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices(),
+ widget;
+
+ if (choices) {
+ var placeholder = (this.optional || this.rmempty)
+ ? E('em', _('unspecified')) : _('-- Please choose --');
+
+ widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ select_placeholder: this.placeholder || placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+ }
+ else {
+ widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
+ id: this.cbid(section_id),
+ password: this.password,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ placeholder: this.placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+ }
+
+ return widget.render();
+ }
+});
+
+var CBIDynamicList = CBIValue.extend({
+ __name__: 'CBI.DynamicList',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices(),
+ items = null;
+
+ if (Array.isArray(value))
+ items = value;
+ else if (value != null)
+ items = String(value).trim().split(/\s+/);
+
+ var widget = new ui.DynamicList(items, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIListValue = CBIValue.extend({
+ __name__: 'CBI.ListValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+ this.widget = 'select';
+ this.deplist = [];
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var choices = this.transformChoices();
+ var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
+ id: this.cbid(section_id),
+ size: this.size,
+ sort: this.keylist,
+ optional: this.rmempty || this.optional,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIFlagValue = CBIValue.extend({
+ __name__: 'CBI.FlagValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+
+ this.enabled = '1';
+ this.disabled = '0';
+ this.default = this.disabled;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
+ id: this.cbid(section_id),
+ value_enabled: this.enabled,
+ value_disabled: this.disabled,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+
+ formvalue: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id)),
+ checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
+
+ return checked ? this.enabled : this.disabled;
+ },
+
+ textvalue: function(section_id) {
+ var cval = this.cfgvalue(section_id);
+
+ if (cval == null)
+ cval = this.default;
+
+ return (cval == this.enabled) ? _('Yes') : _('No');
+ },
+
+ parse: function(section_id) {
+ if (this.isActive(section_id)) {
+ var fval = this.formvalue(section_id);
+
+ if (!this.isValid(section_id))
+ return Promise.reject();
+
+ if (fval == this.default && (this.optional || this.rmempty))
+ return Promise.resolve(this.remove(section_id));
+ else
+ return Promise.resolve(this.write(section_id, fval));
+ }
+ else {
+ return Promise.resolve(this.remove(section_id));
+ }
+ },
+});
+
+var CBIMultiValue = CBIDynamicList.extend({
+ __name__: 'CBI.MultiValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+ this.placeholder = _('-- Please choose --');
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices();
+
+ if (!Array.isArray(value))
+ value = String(value).split(/\s+/);
+
+ var widget = new ui.Dropdown(value, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ multiple: true,
+ optional: this.optional || this.rmempty,
+ select_placeholder: this.placeholder,
+ display_items: this.display_size || this.size || 3,
+ dropdown_items: this.dropdown_size || this.size || 5,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIDummyValue = CBIValue.extend({
+ __name__: 'CBI.DummyValue',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
+ outputEl = E('div');
+
+ if (this.href)
+ outputEl.appendChild(E('a', { 'href': this.href }));
+
+ L.dom.append(outputEl.lastChild || outputEl,
+ this.rawhtml ? value : [ value ]);
+
+ return E([
+ outputEl,
+ hiddenEl.render()
+ ]);
+ },
+});
+
+var CBIButtonValue = CBIValue.extend({
+ __name__: 'CBI.ButtonValue',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default;
+
+ if (value !== false)
+ return E([
+ E('input', {
+ 'type': 'hidden',
+ 'id': this.cbid(section_id)
+ }),
+ E('input', {
+ 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
+ 'type': 'submit',
+ //'id': this.cbid(section_id),
+ //'name': this.cbid(section_id),
+ 'value': this.inputtitle || this.title,
+ 'click': L.bind(function(ev) {
+ ev.target.previousElementSibling.value = ev.target.value;
+ this.map.save();
+ }, this)
+ })
+ ]);
+ else
+ return document.createTextNode(' - ');
+ }
+});
+
+var CBIHiddenValue = CBIValue.extend({
+ __name__: 'CBI.HiddenValue',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
+ id: this.cbid(section_id)
+ });
+
+ return widget.render();
+ }
+});
+
+var CBISectionValue = CBIValue.extend({
+ __name__: 'CBI.ContainerValue',
+ __init__: function(map, section, option, cbiClass /*, ... */) {
+ this.super('__init__', [map, section, option]);
+
+ if (!CBIAbstractSection.isSubclass(cbiClass))
+ throw 'Sub section must be a descendent of CBIAbstractSection';
+
+ this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
+ },
+
+ load: function(section_id) {
+ return this.subsection.load();
+ },
+
+ parse: function(section_id) {
+ return this.subsection.parse();
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ return this.subsection.render();
+ },
+
+ checkDepends: function(section_id) {
+ this.subsection.checkDepends();
+ return this.super('checkDepends');
+ },
+
+ write: function() {},
+ remove: function() {},
+ cfgvalue: function() { return null },
+ formvalue: function() { return null }
+});
+
+return L.Class.extend({
+ Map: CBIMap,
+ AbstractSection: CBIAbstractSection,
+ AbstractValue: CBIAbstractValue,
+
+ TypedSection: CBITypedSection,
+ TableSection: CBITableSection,
+ GridSection: CBIGridSection,
+ NamedSection: CBINamedSection,
+
+ Value: CBIValue,
+ DynamicList: CBIDynamicList,
+ ListValue: CBIListValue,
+ Flag: CBIFlagValue,
+ MultiValue: CBIMultiValue,
+ DummyValue: CBIDummyValue,
+ Button: CBIButtonValue,
+ HiddenValue: CBIHiddenValue,
+ SectionValue: CBISectionValue
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 896ded3af..767d013d7 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -451,7 +451,8 @@
Promise.all([
domReady,
- this.require('ui')
+ this.require('ui'),
+ this.require('form')
]).then(this.setupDOM.bind(this)).catch(function(error) {
alert('LuCI class loading error:\n' + error);
});