9 var CBIJSONConfig = baseclass.extend({
10 __init__: function(data) {
11 data = Object.assign({}, data);
18 for (var sectiontype in data) {
19 if (!data.hasOwnProperty(sectiontype))
22 if (L.isObject(data[sectiontype])) {
23 this.data[sectiontype] = Object.assign(data[sectiontype], {
29 section_ids.push(sectiontype);
32 else if (Array.isArray(data[sectiontype])) {
33 for (var i = 0, index = 0; i < data[sectiontype].length; i++) {
34 var item = data[sectiontype][i],
37 if (!L.isObject(item))
40 if (typeof(item['.name']) == 'string') {
45 name = sectiontype + num_sections;
49 if (!this.data.hasOwnProperty(name))
50 section_ids.push(name);
52 this.data[name] = Object.assign(item, {
53 '.index': num_sections++,
54 '.anonymous': anonymous,
62 section_ids.sort(L.bind(function(a, b) {
63 var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999,
64 indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
67 return (indexA - indexB);
72 for (var i = 0; i < section_ids.length; i++)
73 this.data[section_ids[i]]['.index'] = i;
77 return Promise.resolve(this.data);
81 return Promise.resolve();
84 get: function(config, section, option) {
89 return this.data[section];
91 if (!this.data.hasOwnProperty(section))
94 var value = this.data[section][option];
96 if (Array.isArray(value))
100 return String(value);
105 set: function(config, section, option, value) {
106 if (section == null || option == null || option.charAt(0) == '.')
109 if (!this.data.hasOwnProperty(section))
113 delete this.data[section][option];
114 else if (Array.isArray(value))
115 this.data[section][option] = value;
117 this.data[section][option] = String(value);
120 unset: function(config, section, option) {
121 return this.set(config, section, option, null);
124 sections: function(config, sectiontype, callback) {
127 for (var section_id in this.data)
128 if (sectiontype == null || this.data[section_id]['.type'] == sectiontype)
129 rv.push(this.data[section_id]);
131 rv.sort(function(a, b) { return a['.index'] - b['.index'] });
133 if (typeof(callback) == 'function')
134 for (var i = 0; i < rv.length; i++)
135 callback.call(this, rv[i], rv[i]['.name']);
140 add: function(config, sectiontype, sectionname) {
141 var num_sections_type = 0, next_index = 0;
143 for (var name in this.data) {
144 num_sections_type += (this.data[name]['.type'] == sectiontype);
145 next_index = Math.max(next_index, this.data[name]['.index']);
148 var section_id = sectionname || sectiontype + num_sections_type;
150 if (!this.data.hasOwnProperty(section_id)) {
151 this.data[section_id] = {
153 '.type': sectiontype,
154 '.anonymous': (sectionname == null),
155 '.index': next_index + 1
162 remove: function(config, section) {
163 if (this.data.hasOwnProperty(section))
164 delete this.data[section];
167 resolveSID: function(config, section_id) {
171 move: function(config, section_id1, section_id2, after) {
172 return uci.move.apply(this, [config, section_id1, section_id2, after]);
176 var CBINode = baseclass.extend({
177 __init__: function(title, description) {
178 this.title = title || '';
179 this.description = description || '';
183 append: function(obj) {
184 this.children.push(obj);
188 var args = arguments;
189 this.children.forEach(function(child) {
190 child.parse.apply(child, args);
195 L.error('InternalError', 'Not implemented');
198 loadChildren: function(/* ... */) {
201 if (Array.isArray(this.children))
202 for (var i = 0; i < this.children.length; i++)
203 if (!this.children[i].disable)
204 tasks.push(this.children[i].load.apply(this.children[i], arguments));
206 return Promise.all(tasks);
209 renderChildren: function(tab_name /*, ... */) {
213 if (Array.isArray(this.children))
214 for (var i = 0; i < this.children.length; i++)
215 if (tab_name === null || this.children[i].tab === tab_name)
216 if (!this.children[i].disable)
217 tasks.push(this.children[i].render.apply(
218 this.children[i], this.varargs(arguments, 1, index++)));
220 return Promise.all(tasks);
223 stripTags: function(s) {
224 if (typeof(s) == 'string' && !s.match(/[<>]/))
227 var x = E('div', {}, s);
228 return x.textContent || x.innerText || '';
231 titleFn: function(attr /*, ... */) {
234 if (typeof(this[attr]) == 'function')
235 s = this[attr].apply(this, this.varargs(arguments, 1));
236 else if (typeof(this[attr]) == 'string')
237 s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
240 s = this.stripTags(String(s)).trim();
242 if (s == null || s == '')
249 var CBIMap = CBINode.extend({
250 __init__: function(config /*, ... */) {
251 this.super('__init__', this.varargs(arguments, 1));
253 this.config = config;
254 this.parsechain = [ config ];
258 findElements: function(/* ... */) {
261 if (arguments.length == 1)
263 else if (arguments.length == 2)
264 q = '[%s="%s"]'.format(arguments[0], arguments[1]);
266 L.error('InternalError', 'Expecting one or two arguments to findElements()');
268 return this.root.querySelectorAll(q);
271 findElement: function(/* ... */) {
272 var res = this.findElements.apply(this, arguments);
273 return res.length ? res[0] : null;
276 chain: function(config) {
277 if (this.parsechain.indexOf(config) == -1)
278 this.parsechain.push(config);
281 section: function(cbiClass /*, ... */) {
282 if (!CBIAbstractSection.isSubclass(cbiClass))
283 L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
285 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
291 return this.data.load(this.parsechain || [ this.config ])
292 .then(this.loadChildren.bind(this));
298 if (Array.isArray(this.children))
299 for (var i = 0; i < this.children.length; i++)
300 tasks.push(this.children[i].parse());
302 return Promise.all(tasks);
305 save: function(cb, silent) {
310 .then(this.data.save.bind(this.data))
311 .then(this.load.bind(this))
314 alert('Cannot save due to invalid values');
316 return Promise.reject();
317 }).finally(this.renderContents.bind(this));
321 return this.renderContents();
325 return this.load().then(this.renderContents.bind(this));
328 renderContents: function() {
329 var mapEl = this.root || (this.root = E('div', {
330 'id': 'cbi-%s'.format(this.config),
332 'cbi-dependency-check': L.bind(this.checkDepends, this)
335 dom.bindClassInstance(mapEl, this);
337 return this.renderChildren(null).then(L.bind(function(nodes) {
338 var initialRender = !mapEl.firstChild;
340 dom.content(mapEl, null);
342 if (this.title != null && this.title != '')
343 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
345 if (this.description != null && this.description != '')
346 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
349 dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
351 dom.append(mapEl, nodes);
353 if (!initialRender) {
354 mapEl.classList.remove('flash');
356 window.setTimeout(function() {
357 mapEl.classList.add('flash');
363 var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
365 for (var i = 0; i < tabGroups.length; i++)
366 ui.tabs.initTabGroup(tabGroups[i].childNodes);
372 lookupOption: function(name, section_id, config_name) {
373 var id, elem, sid, inst;
375 if (name.indexOf('.') > -1)
376 id = 'cbid.%s'.format(name);
378 id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
380 elem = this.findElement('data-field', id);
381 sid = elem ? id.split(/\./)[2] : null;
382 inst = elem ? dom.findClassInstance(elem) : null;
384 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
387 checkDepends: function(ev, n) {
390 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
391 if (s.checkDepends(ev, n))
394 if (changed && (n || 0) < 10)
395 this.checkDepends(ev, (n || 10) + 1);
397 ui.tabs.updateTabs(ev, this.root);
400 isDependencySatisfied: function(depends, config_name, section_id) {
403 if (!Array.isArray(depends) || !depends.length)
406 for (var i = 0; i < depends.length; i++) {
408 reverse = depends[i]['!reverse'],
409 contains = depends[i]['!contains'];
411 for (var dep in depends[i]) {
412 if (dep == '!reverse' || dep == '!contains') {
415 else if (dep == '!default') {
420 var res = this.lookupOption(dep, section_id, config_name),
421 val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
424 ? isContained(val, depends[i][dep])
425 : isEqual(val, depends[i][dep]);
427 istat = (istat && equal);
439 var CBIJSONMap = CBIMap.extend({
440 __init__: function(data /*, ... */) {
441 this.super('__init__', this.varargs(arguments, 1, 'json'));
443 this.config = 'json';
444 this.parsechain = [ 'json' ];
445 this.data = new CBIJSONConfig(data);
449 var CBIAbstractSection = CBINode.extend({
450 __init__: function(map, sectionType /*, ... */) {
451 this.super('__init__', this.varargs(arguments, 2));
453 this.sectiontype = sectionType;
455 this.config = map.config;
457 this.optional = true;
458 this.addremove = false;
459 this.dynamic = false;
462 cfgsections: function() {
463 L.error('InternalError', 'Not implemented');
466 filter: function(section_id) {
471 var section_ids = this.cfgsections(),
474 if (Array.isArray(this.children))
475 for (var i = 0; i < section_ids.length; i++)
476 tasks.push(this.loadChildren(section_ids[i])
477 .then(Function.prototype.bind.call(function(section_id, set_values) {
478 for (var i = 0; i < set_values.length; i++)
479 this.children[i].cfgvalue(section_id, set_values[i]);
480 }, this, section_ids[i])));
482 return Promise.all(tasks);
486 var section_ids = this.cfgsections(),
489 if (Array.isArray(this.children))
490 for (var i = 0; i < section_ids.length; i++)
491 for (var j = 0; j < this.children.length; j++)
492 tasks.push(this.children[j].parse(section_ids[i]));
494 return Promise.all(tasks);
497 tab: function(name, title, description) {
498 if (this.tabs && this.tabs[name])
499 throw 'Tab already declared';
504 description: description,
508 this.tabs = this.tabs || [];
509 this.tabs.push(entry);
510 this.tabs[name] = entry;
512 this.tab_names = this.tab_names || [];
513 this.tab_names.push(name);
516 option: function(cbiClass /*, ... */) {
517 if (!CBIAbstractValue.isSubclass(cbiClass))
518 throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
520 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
525 taboption: function(tabName /*, ... */) {
526 if (!this.tabs || !this.tabs[tabName])
527 throw L.error('ReferenceError', 'Associated tab not declared');
529 var obj = this.option.apply(this, this.varargs(arguments, 1));
531 this.tabs[tabName].children.push(obj);
535 renderUCISection: function(section_id) {
536 var renderTasks = [];
539 return this.renderOptions(null, section_id);
541 for (var i = 0; i < this.tab_names.length; i++)
542 renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
544 return Promise.all(renderTasks)
545 .then(this.renderTabContainers.bind(this, section_id));
548 renderTabContainers: function(section_id, nodes) {
549 var config_name = this.uciconfig || this.map.config,
550 containerEls = E([]);
552 for (var i = 0; i < nodes.length; i++) {
553 var tab_name = this.tab_names[i],
554 tab_data = this.tabs[tab_name],
555 containerEl = E('div', {
556 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
557 'data-tab': tab_name,
558 'data-tab-title': tab_data.title,
559 'data-tab-active': tab_name === this.selected_tab
562 if (tab_data.description != null && tab_data.description != '')
563 containerEl.appendChild(
564 E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
566 containerEl.appendChild(nodes[i]);
567 containerEls.appendChild(containerEl);
573 renderOptions: function(tab_name, section_id) {
574 var in_table = (this instanceof CBITableSection);
575 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
576 var optionEls = E([]);
577 for (var i = 0; i < nodes.length; i++)
578 optionEls.appendChild(nodes[i]);
583 checkDepends: function(ev, n) {
585 sids = this.cfgsections();
587 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
588 for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
589 var isActive = o.isActive(sid),
590 isSatisified = o.checkDepends(sid);
592 if (isActive != isSatisified) {
593 o.setActive(sid, !isActive);
594 isActive = !isActive;
599 o.triggerValidation(sid);
608 var isEqual = function(x, y) {
609 if (x != null && y != null && typeof(x) != typeof(y))
612 if ((x == null && y != null) || (x != null && y == null))
615 if (Array.isArray(x)) {
616 if (x.length != y.length)
619 for (var i = 0; i < x.length; i++)
620 if (!isEqual(x[i], y[i]))
623 else if (typeof(x) == 'object') {
625 if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
628 if (!isEqual(x[k], y[k]))
633 if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
643 var isContained = function(x, y) {
644 if (Array.isArray(x)) {
645 for (var i = 0; i < x.length; i++)
649 else if (L.isObject(x)) {
650 if (x.hasOwnProperty(y) && x[y] != null)
653 else if (typeof(x) == 'string') {
654 return (x.indexOf(y) > -1);
660 var CBIAbstractValue = CBINode.extend({
661 __init__: function(map, section, option /*, ... */) {
662 this.super('__init__', this.varargs(arguments, 3));
664 this.section = section;
665 this.option = option;
667 this.config = map.config;
674 this.optional = false;
677 depends: function(field, value) {
680 if (typeof(field) === 'string')
681 deps = {}, deps[field] = value;
685 this.deps.push(deps);
688 transformDepList: function(section_id, deplist) {
689 var list = deplist || this.deps,
692 if (Array.isArray(list)) {
693 for (var i = 0; i < list.length; i++) {
696 for (var k in list[i]) {
697 if (list[i].hasOwnProperty(k)) {
698 if (k.charAt(0) === '!')
700 else if (k.indexOf('.') !== -1)
701 dep['cbid.%s'.format(k)] = list[i][k];
703 dep['cbid.%s.%s.%s'.format(
704 this.uciconfig || this.section.uciconfig || this.map.config,
705 this.ucisection || section_id,
712 if (dep.hasOwnProperty(k)) {
723 transformChoices: function() {
724 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
729 for (var i = 0; i < this.keylist.length; i++)
730 choices[this.keylist[i]] = this.vallist[i];
735 checkDepends: function(section_id) {
736 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
737 active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
740 this.updateDefaultValue(section_id);
745 updateDefaultValue: function(section_id) {
746 if (!L.isObject(this.defaults))
749 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
750 cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
751 default_defval = null, satisified_defval = null;
753 for (var value in this.defaults) {
754 if (!this.defaults[value] || this.defaults[value].length == 0) {
755 default_defval = value;
758 else if (this.map.isDependencySatisfied(this.defaults[value], config_name, section_id)) {
759 satisified_defval = value;
764 if (satisified_defval == null)
765 satisified_defval = default_defval;
767 var node = this.map.findElement('id', this.cbid(section_id));
768 if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
769 dom.callClassMethod(node, 'setValue', satisified_defval);
771 this.default = satisified_defval;
774 cbid: function(section_id) {
775 if (section_id == null)
776 L.error('TypeError', 'Section ID required');
778 return 'cbid.%s.%s.%s'.format(
779 this.uciconfig || this.section.uciconfig || this.map.config,
780 section_id, this.option);
783 load: function(section_id) {
784 if (section_id == null)
785 L.error('TypeError', 'Section ID required');
787 return this.map.data.get(
788 this.uciconfig || this.section.uciconfig || this.map.config,
789 this.ucisection || section_id,
790 this.ucioption || this.option);
793 getUIElement: function(section_id) {
794 var node = this.map.findElement('id', this.cbid(section_id)),
795 inst = node ? dom.findClassInstance(node) : null;
796 return (inst instanceof ui.AbstractElement) ? inst : null;
799 cfgvalue: function(section_id, set_value) {
800 if (section_id == null)
801 L.error('TypeError', 'Section ID required');
803 if (arguments.length == 2) {
804 this.data = this.data || {};
805 this.data[section_id] = set_value;
808 return this.data ? this.data[section_id] : null;
811 formvalue: function(section_id) {
812 var elem = this.getUIElement(section_id);
813 return elem ? elem.getValue() : null;
816 textvalue: function(section_id) {
817 var cval = this.cfgvalue(section_id);
822 return (cval != null) ? '%h'.format(cval) : null;
825 validate: function(section_id, value) {
829 isValid: function(section_id) {
830 var elem = this.getUIElement(section_id);
831 return elem ? elem.isValid() : true;
834 isActive: function(section_id) {
835 var field = this.map.findElement('data-field', this.cbid(section_id));
836 return (field != null && !field.classList.contains('hidden'));
839 setActive: function(section_id, active) {
840 var field = this.map.findElement('data-field', this.cbid(section_id));
842 if (field && field.classList.contains('hidden') == active) {
843 field.classList[active ? 'remove' : 'add']('hidden');
850 triggerValidation: function(section_id) {
851 var elem = this.getUIElement(section_id);
852 return elem ? elem.triggerValidation() : true;
855 parse: function(section_id) {
856 var active = this.isActive(section_id),
857 cval = this.cfgvalue(section_id),
858 fval = active ? this.formvalue(section_id) : null;
860 if (active && !this.isValid(section_id))
861 return Promise.reject();
863 if (fval != '' && fval != null) {
864 if (this.forcewrite || !isEqual(cval, fval))
865 return Promise.resolve(this.write(section_id, fval));
868 if (!active || this.rmempty || this.optional) {
869 return Promise.resolve(this.remove(section_id));
871 else if (!isEqual(cval, fval)) {
872 console.log('This should have been catched by isValid()');
873 return Promise.reject();
877 return Promise.resolve();
880 write: function(section_id, formvalue) {
881 return this.map.data.set(
882 this.uciconfig || this.section.uciconfig || this.map.config,
883 this.ucisection || section_id,
884 this.ucioption || this.option,
888 remove: function(section_id) {
889 return this.map.data.unset(
890 this.uciconfig || this.section.uciconfig || this.map.config,
891 this.ucisection || section_id,
892 this.ucioption || this.option);
896 var CBITypedSection = CBIAbstractSection.extend({
897 __name__: 'CBI.TypedSection',
899 cfgsections: function() {
900 return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
901 .map(function(s) { return s['.name'] })
902 .filter(L.bind(this.filter, this));
905 handleAdd: function(ev, name) {
906 var config_name = this.uciconfig || this.map.config;
908 this.map.data.add(config_name, this.sectiontype, name);
909 return this.map.save(null, true);
912 handleRemove: function(section_id, ev) {
913 var config_name = this.uciconfig || this.map.config;
915 this.map.data.remove(config_name, section_id);
916 return this.map.save(null, true);
919 renderSectionAdd: function(extra_class) {
923 var createEl = E('div', { 'class': 'cbi-section-create' }),
924 config_name = this.uciconfig || this.map.config,
925 btn_title = this.titleFn('addbtntitle');
927 if (extra_class != null)
928 createEl.classList.add(extra_class);
930 if (this.anonymous) {
931 createEl.appendChild(E('button', {
932 'class': 'cbi-button cbi-button-add',
933 'title': btn_title || _('Add'),
934 'click': ui.createHandlerFn(this, 'handleAdd')
935 }, [ btn_title || _('Add') ]));
938 var nameEl = E('input', {
940 'class': 'cbi-section-create-name'
943 dom.append(createEl, [
944 E('div', {}, nameEl),
946 'class': 'cbi-button cbi-button-add',
948 'value': btn_title || _('Add'),
949 'title': btn_title || _('Add'),
950 'click': ui.createHandlerFn(this, function(ev) {
951 if (nameEl.classList.contains('cbi-input-invalid'))
954 return this.handleAdd(ev, nameEl.value);
959 ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
965 renderSectionPlaceholder: function() {
967 E('em', _('This section contains no values yet')),
972 renderContents: function(cfgsections, nodes) {
973 var section_id = null,
974 config_name = this.uciconfig || this.map.config,
975 sectionEl = E('div', {
976 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
977 'class': 'cbi-section',
978 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
979 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
982 if (this.title != null && this.title != '')
983 sectionEl.appendChild(E('legend', {}, this.title));
985 if (this.description != null && this.description != '')
986 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
988 for (var i = 0; i < nodes.length; i++) {
989 if (this.addremove) {
990 sectionEl.appendChild(
991 E('div', { 'class': 'cbi-section-remove right' },
993 'class': 'cbi-button',
994 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
995 'data-section-id': cfgsections[i],
996 'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
997 }, [ _('Delete') ])));
1000 if (!this.anonymous)
1001 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
1003 sectionEl.appendChild(E('div', {
1004 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
1006 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1007 'data-section-id': cfgsections[i]
1011 if (nodes.length == 0)
1012 sectionEl.appendChild(this.renderSectionPlaceholder());
1014 sectionEl.appendChild(this.renderSectionAdd());
1016 dom.bindClassInstance(sectionEl, this);
1021 render: function() {
1022 var cfgsections = this.cfgsections(),
1025 for (var i = 0; i < cfgsections.length; i++)
1026 renderTasks.push(this.renderUCISection(cfgsections[i]));
1028 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
1032 var CBITableSection = CBITypedSection.extend({
1033 __name__: 'CBI.TableSection',
1036 throw 'Tabs are not supported by TableSection';
1039 renderContents: function(cfgsections, nodes) {
1040 var section_id = null,
1041 config_name = this.uciconfig || this.map.config,
1042 max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1043 has_more = max_cols < this.children.length,
1044 sectionEl = E('div', {
1045 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
1046 'class': 'cbi-section cbi-tblsection',
1047 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
1048 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
1050 tableEl = E('div', {
1051 'class': 'table cbi-section-table'
1054 if (this.title != null && this.title != '')
1055 sectionEl.appendChild(E('h3', {}, this.title));
1057 if (this.description != null && this.description != '')
1058 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1060 tableEl.appendChild(this.renderHeaderRows(max_cols));
1062 for (var i = 0; i < nodes.length; i++) {
1063 var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
1065 if (sectionname == null)
1066 sectionname = cfgsections[i];
1068 var trEl = E('div', {
1069 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
1070 'class': 'tr cbi-section-table-row',
1071 'data-sid': cfgsections[i],
1072 'draggable': this.sortable ? true : null,
1073 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
1074 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
1075 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
1076 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
1077 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
1078 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
1079 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
1080 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
1081 'data-section-id': cfgsections[i]
1084 if (this.extedit || this.rowcolors)
1085 trEl.classList.add(!(tableEl.childNodes.length % 2)
1086 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
1088 for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
1089 trEl.appendChild(nodes[i].firstChild);
1091 trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
1092 tableEl.appendChild(trEl);
1095 if (nodes.length == 0)
1096 tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
1097 E('div', { 'class': 'td' },
1098 E('em', {}, _('This section contains no values yet')))));
1100 sectionEl.appendChild(tableEl);
1102 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
1104 dom.bindClassInstance(sectionEl, this);
1109 renderHeaderRows: function(max_cols, has_action) {
1110 var has_titles = false,
1111 has_descriptions = false,
1112 max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1113 has_more = max_cols < this.children.length,
1114 anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
1117 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1121 has_titles = has_titles || !!opt.title;
1122 has_descriptions = has_descriptions || !!opt.description;
1126 var trEl = E('div', {
1127 'class': 'tr cbi-section-table-titles ' + anon_class,
1128 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
1131 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1135 trEl.appendChild(E('div', {
1136 'class': 'th cbi-section-table-cell',
1137 'data-widget': opt.__name__
1140 if (opt.width != null)
1141 trEl.lastElementChild.style.width =
1142 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1145 trEl.lastElementChild.appendChild(E('a', {
1146 'href': opt.titleref,
1147 'class': 'cbi-title-ref',
1148 'title': this.titledesc || _('Go to relevant configuration page')
1151 dom.content(trEl.lastElementChild, opt.title);
1154 if (this.sortable || this.extedit || this.addremove || has_more || has_action)
1155 trEl.appendChild(E('div', {
1156 'class': 'th cbi-section-table-cell cbi-section-actions'
1159 trEls.appendChild(trEl);
1162 if (has_descriptions) {
1163 var trEl = E('div', {
1164 'class': 'tr cbi-section-table-descr ' + anon_class
1167 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1171 trEl.appendChild(E('div', {
1172 'class': 'th cbi-section-table-cell',
1173 'data-widget': opt.__name__
1174 }, opt.description));
1176 if (opt.width != null)
1177 trEl.lastElementChild.style.width =
1178 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1181 if (this.sortable || this.extedit || this.addremove || has_more)
1182 trEl.appendChild(E('div', {
1183 'class': 'th cbi-section-table-cell cbi-section-actions'
1186 trEls.appendChild(trEl);
1192 renderRowActions: function(section_id, more_label) {
1193 var config_name = this.uciconfig || this.map.config;
1195 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
1198 var tdEl = E('div', {
1199 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
1202 if (this.sortable) {
1203 dom.append(tdEl.lastElementChild, [
1205 'title': _('Drag to reorder'),
1206 'class': 'cbi-button drag-handle center',
1207 'style': 'cursor:move'
1215 if (typeof(this.extedit) == 'function')
1216 evFn = L.bind(this.extedit, this);
1217 else if (typeof(this.extedit) == 'string')
1218 evFn = L.bind(function(sid, ev) {
1219 location.href = this.extedit.format(sid);
1220 }, this, section_id);
1222 dom.append(tdEl.lastElementChild,
1225 'class': 'cbi-button cbi-button-edit',
1232 dom.append(tdEl.lastElementChild,
1234 'title': more_label,
1235 'class': 'cbi-button cbi-button-edit',
1236 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
1241 if (this.addremove) {
1242 var btn_title = this.titleFn('removebtntitle', section_id);
1244 dom.append(tdEl.lastElementChild,
1246 'title': btn_title || _('Delete'),
1247 'class': 'cbi-button cbi-button-remove',
1248 'click': ui.createHandlerFn(this, 'handleRemove', section_id)
1249 }, [ btn_title || _('Delete') ])
1256 handleDragInit: function(ev) {
1257 scope.dragState = { node: ev.target };
1260 handleDragStart: function(ev) {
1261 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1262 scope.dragState = null;
1263 ev.preventDefault();
1267 scope.dragState.node = dom.parent(scope.dragState.node, '.tr');
1268 ev.dataTransfer.setData('text', 'drag');
1269 ev.target.style.opacity = 0.4;
1272 handleDragOver: function(ev) {
1273 var n = scope.dragState.targetNode,
1274 r = scope.dragState.rect,
1275 t = r.top + r.height / 2;
1277 if (ev.clientY <= t) {
1278 n.classList.remove('drag-over-below');
1279 n.classList.add('drag-over-above');
1282 n.classList.remove('drag-over-above');
1283 n.classList.add('drag-over-below');
1286 ev.dataTransfer.dropEffect = 'move';
1287 ev.preventDefault();
1291 handleDragEnter: function(ev) {
1292 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1293 scope.dragState.targetNode = ev.currentTarget;
1296 handleDragLeave: function(ev) {
1297 ev.currentTarget.classList.remove('drag-over-above');
1298 ev.currentTarget.classList.remove('drag-over-below');
1301 handleDragEnd: function(ev) {
1304 n.style.opacity = '';
1305 n.classList.add('flash');
1306 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1307 .forEach(function(tr) {
1308 tr.classList.remove('drag-over-above');
1309 tr.classList.remove('drag-over-below');
1313 handleDrop: function(ev) {
1314 var s = scope.dragState;
1316 if (s.node && s.targetNode) {
1317 var config_name = this.uciconfig || this.map.config,
1318 ref_node = s.targetNode,
1321 if (ref_node.classList.contains('drag-over-below')) {
1322 ref_node = ref_node.nextElementSibling;
1326 var sid1 = s.node.getAttribute('data-sid'),
1327 sid2 = s.targetNode.getAttribute('data-sid');
1329 s.node.parentNode.insertBefore(s.node, ref_node);
1330 this.map.data.move(config_name, sid1, sid2, after);
1333 scope.dragState = null;
1334 ev.target.style.opacity = '';
1335 ev.stopPropagation();
1336 ev.preventDefault();
1340 handleModalCancel: function(modalMap, ev) {
1341 return Promise.resolve(ui.hideModal());
1344 handleModalSave: function(modalMap, ev) {
1345 return modalMap.save()
1346 .then(L.bind(this.map.load, this.map))
1347 .then(L.bind(this.map.reset, this.map))
1349 .catch(function() {});
1352 addModalOptions: function(modalSection, section_id, ev) {
1356 renderMoreOptionsModal: function(section_id, ev) {
1357 var parent = this.map,
1358 title = parent.title,
1360 m = new CBIMap(this.map.config, null, null),
1361 s = m.section(CBINamedSection, section_id, this.sectiontype);
1366 s.tab_names = this.tab_names;
1368 if ((name = this.titleFn('modaltitle', section_id)) != null)
1370 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1371 title = '%s - %s'.format(parent.title, name);
1372 else if (!this.anonymous)
1373 title = '%s - %s'.format(parent.title, section_id);
1375 for (var i = 0; i < this.children.length; i++) {
1376 var o1 = this.children[i];
1378 if (o1.modalonly === false)
1381 var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1384 if (!o1.hasOwnProperty(k))
1401 return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1402 ui.showModal(title, [
1404 E('div', { 'class': 'right' }, [
1407 'click': ui.createHandlerFn(this, 'handleModalCancel', m)
1408 }, [ _('Dismiss') ]), ' ',
1410 'class': 'cbi-button cbi-button-positive important',
1411 'click': ui.createHandlerFn(this, 'handleModalSave', m)
1415 }, this)).catch(L.error);
1419 var CBIGridSection = CBITableSection.extend({
1420 tab: function(name, title, description) {
1421 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1424 handleAdd: function(ev, name) {
1425 var config_name = this.uciconfig || this.map.config,
1426 section_id = this.map.data.add(config_name, this.sectiontype, name);
1428 this.addedSection = section_id;
1429 return this.renderMoreOptionsModal(section_id);
1432 handleModalSave: function(/* ... */) {
1433 return this.super('handleModalSave', arguments)
1434 .then(L.bind(function() { this.addedSection = null }, this));
1437 handleModalCancel: function(/* ... */) {
1438 var config_name = this.uciconfig || this.map.config;
1440 if (this.addedSection != null) {
1441 this.map.data.remove(config_name, this.addedSection);
1442 this.addedSection = null;
1445 return this.super('handleModalCancel', arguments);
1448 renderUCISection: function(section_id) {
1449 return this.renderOptions(null, section_id);
1452 renderChildren: function(tab_name, section_id, in_table) {
1453 var tasks = [], index = 0;
1455 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1456 if (opt.disable || opt.modalonly)
1460 tasks.push(opt.render(index++, section_id, in_table));
1462 tasks.push(this.renderTextValue(section_id, opt));
1465 return Promise.all(tasks);
1468 renderTextValue: function(section_id, opt) {
1469 var title = this.stripTags(opt.title).trim(),
1470 descr = this.stripTags(opt.description).trim(),
1471 value = opt.textvalue(section_id);
1474 'class': 'td cbi-value-field',
1475 'data-title': (title != '') ? title : null,
1476 'data-description': (descr != '') ? descr : null,
1477 'data-name': opt.option,
1478 'data-widget': opt.typename || opt.__name__
1479 }, (value != null) ? value : E('em', _('none')));
1482 renderHeaderRows: function(section_id) {
1483 return this.super('renderHeaderRows', [ NaN, true ]);
1486 renderRowActions: function(section_id) {
1487 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1491 var section_ids = this.cfgsections(),
1494 if (Array.isArray(this.children)) {
1495 for (var i = 0; i < section_ids.length; i++) {
1496 for (var j = 0; j < this.children.length; j++) {
1497 if (!this.children[j].editable || this.children[j].modalonly)
1500 tasks.push(this.children[j].parse(section_ids[i]));
1505 return Promise.all(tasks);
1509 var CBINamedSection = CBIAbstractSection.extend({
1510 __name__: 'CBI.NamedSection',
1511 __init__: function(map, section_id /*, ... */) {
1512 this.super('__init__', this.varargs(arguments, 2, map));
1514 this.section = section_id;
1517 cfgsections: function() {
1518 return [ this.section ];
1521 handleAdd: function(ev) {
1522 var section_id = this.section,
1523 config_name = this.uciconfig || this.map.config;
1525 this.map.data.add(config_name, this.sectiontype, section_id);
1526 return this.map.save(null, true);
1529 handleRemove: function(ev) {
1530 var section_id = this.section,
1531 config_name = this.uciconfig || this.map.config;
1533 this.map.data.remove(config_name, section_id);
1534 return this.map.save(null, true);
1537 renderContents: function(data) {
1538 var ucidata = data[0], nodes = data[1],
1539 section_id = this.section,
1540 config_name = this.uciconfig || this.map.config,
1541 sectionEl = E('div', {
1542 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1543 'class': 'cbi-section',
1544 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
1545 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
1548 if (typeof(this.title) === 'string' && this.title !== '')
1549 sectionEl.appendChild(E('legend', {}, this.title));
1551 if (typeof(this.description) === 'string' && this.description !== '')
1552 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1555 if (this.addremove) {
1556 sectionEl.appendChild(
1557 E('div', { 'class': 'cbi-section-remove right' },
1559 'class': 'cbi-button',
1560 'click': ui.createHandlerFn(this, 'handleRemove')
1561 }, [ _('Delete') ])));
1564 sectionEl.appendChild(E('div', {
1565 'id': 'cbi-%s-%s'.format(config_name, section_id),
1567 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1568 'data-section-id': section_id
1571 else if (this.addremove) {
1572 sectionEl.appendChild(
1574 'class': 'cbi-button cbi-button-add',
1575 'click': ui.createHandlerFn(this, 'handleAdd')
1579 dom.bindClassInstance(sectionEl, this);
1584 render: function() {
1585 var config_name = this.uciconfig || this.map.config,
1586 section_id = this.section;
1588 return Promise.all([
1589 this.map.data.get(config_name, section_id),
1590 this.renderUCISection(section_id)
1591 ]).then(this.renderContents.bind(this));
1595 var CBIValue = CBIAbstractValue.extend({
1596 __name__: 'CBI.Value',
1598 value: function(key, val) {
1599 this.keylist = this.keylist || [];
1600 this.keylist.push(String(key));
1602 this.vallist = this.vallist || [];
1603 this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
1606 render: function(option_index, section_id, in_table) {
1607 return Promise.resolve(this.cfgvalue(section_id))
1608 .then(this.renderWidget.bind(this, section_id, option_index))
1609 .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1612 renderFrame: function(section_id, in_table, option_index, nodes) {
1613 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1614 depend_list = this.transformDepList(section_id),
1618 var title = this.stripTags(this.title).trim();
1619 optionEl = E('div', {
1620 'class': 'td cbi-value-field',
1621 'data-title': (title != '') ? title : null,
1622 'data-description': this.stripTags(this.description).trim(),
1623 'data-name': this.option,
1624 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1626 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1627 'data-index': option_index,
1628 'data-depends': depend_list,
1629 'data-field': this.cbid(section_id)
1633 optionEl = E('div', {
1634 'class': 'cbi-value',
1635 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1636 'data-index': option_index,
1637 'data-depends': depend_list,
1638 'data-field': this.cbid(section_id),
1639 'data-name': this.option,
1640 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1643 if (this.last_child)
1644 optionEl.classList.add('cbi-value-last');
1646 if (typeof(this.title) === 'string' && this.title !== '') {
1647 optionEl.appendChild(E('label', {
1648 'class': 'cbi-value-title',
1649 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
1650 'click': function(ev) {
1651 var node = ev.currentTarget,
1652 elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
1660 this.titleref ? E('a', {
1661 'class': 'cbi-title-ref',
1662 'href': this.titleref,
1663 'title': this.titledesc || _('Go to relevant configuration page')
1664 }, this.title) : this.title));
1666 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1671 (optionEl.lastChild || optionEl).appendChild(nodes);
1673 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1674 dom.append(optionEl.lastChild || optionEl,
1675 E('div', { 'class': 'cbi-value-description' }, this.description));
1677 if (depend_list && depend_list.length)
1678 optionEl.classList.add('hidden');
1680 optionEl.addEventListener('widget-change',
1681 L.bind(this.map.checkDepends, this.map));
1683 dom.bindClassInstance(optionEl, this);
1688 renderWidget: function(section_id, option_index, cfgvalue) {
1689 var value = (cfgvalue != null) ? cfgvalue : this.default,
1690 choices = this.transformChoices(),
1694 var placeholder = (this.optional || this.rmempty)
1695 ? E('em', _('unspecified')) : _('-- Please choose --');
1697 widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1698 id: this.cbid(section_id),
1700 optional: this.optional || this.rmempty,
1701 datatype: this.datatype,
1702 select_placeholder: this.placeholder || placeholder,
1703 validate: L.bind(this.validate, this, section_id)
1707 widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1708 id: this.cbid(section_id),
1709 password: this.password,
1710 optional: this.optional || this.rmempty,
1711 datatype: this.datatype,
1712 placeholder: this.placeholder,
1713 validate: L.bind(this.validate, this, section_id)
1717 return widget.render();
1721 var CBIDynamicList = CBIValue.extend({
1722 __name__: 'CBI.DynamicList',
1724 renderWidget: function(section_id, option_index, cfgvalue) {
1725 var value = (cfgvalue != null) ? cfgvalue : this.default,
1726 choices = this.transformChoices(),
1727 items = L.toArray(value);
1729 var widget = new ui.DynamicList(items, choices, {
1730 id: this.cbid(section_id),
1732 optional: this.optional || this.rmempty,
1733 datatype: this.datatype,
1734 placeholder: this.placeholder,
1735 validate: L.bind(this.validate, this, section_id)
1738 return widget.render();
1742 var CBIListValue = CBIValue.extend({
1743 __name__: 'CBI.ListValue',
1745 __init__: function() {
1746 this.super('__init__', arguments);
1747 this.widget = 'select';
1751 renderWidget: function(section_id, option_index, cfgvalue) {
1752 var choices = this.transformChoices();
1753 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1754 id: this.cbid(section_id),
1757 optional: this.optional,
1758 placeholder: this.placeholder,
1759 validate: L.bind(this.validate, this, section_id)
1762 return widget.render();
1766 var CBIFlagValue = CBIValue.extend({
1767 __name__: 'CBI.FlagValue',
1769 __init__: function() {
1770 this.super('__init__', arguments);
1773 this.disabled = '0';
1774 this.default = this.disabled;
1777 renderWidget: function(section_id, option_index, cfgvalue) {
1778 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1779 id: this.cbid(section_id),
1780 value_enabled: this.enabled,
1781 value_disabled: this.disabled,
1782 validate: L.bind(this.validate, this, section_id)
1785 return widget.render();
1788 formvalue: function(section_id) {
1789 var elem = this.getUIElement(section_id),
1790 checked = elem ? elem.isChecked() : false;
1791 return checked ? this.enabled : this.disabled;
1794 textvalue: function(section_id) {
1795 var cval = this.cfgvalue(section_id);
1798 cval = this.default;
1800 return (cval == this.enabled) ? _('Yes') : _('No');
1803 parse: function(section_id) {
1804 if (this.isActive(section_id)) {
1805 var fval = this.formvalue(section_id);
1807 if (!this.isValid(section_id))
1808 return Promise.reject();
1810 if (fval == this.default && (this.optional || this.rmempty))
1811 return Promise.resolve(this.remove(section_id));
1813 return Promise.resolve(this.write(section_id, fval));
1816 return Promise.resolve(this.remove(section_id));
1821 var CBIMultiValue = CBIDynamicList.extend({
1822 __name__: 'CBI.MultiValue',
1824 __init__: function() {
1825 this.super('__init__', arguments);
1826 this.placeholder = _('-- Please choose --');
1829 renderWidget: function(section_id, option_index, cfgvalue) {
1830 var value = (cfgvalue != null) ? cfgvalue : this.default,
1831 choices = this.transformChoices();
1833 var widget = new ui.Dropdown(L.toArray(value), choices, {
1834 id: this.cbid(section_id),
1837 optional: this.optional || this.rmempty,
1838 select_placeholder: this.placeholder,
1839 display_items: this.display_size || this.size || 3,
1840 dropdown_items: this.dropdown_size || this.size || -1,
1841 validate: L.bind(this.validate, this, section_id)
1844 return widget.render();
1848 var CBITextValue = CBIValue.extend({
1849 __name__: 'CBI.TextValue',
1853 renderWidget: function(section_id, option_index, cfgvalue) {
1854 var value = (cfgvalue != null) ? cfgvalue : this.default;
1856 var widget = new ui.Textarea(value, {
1857 id: this.cbid(section_id),
1858 optional: this.optional || this.rmempty,
1859 placeholder: this.placeholder,
1860 monospace: this.monospace,
1864 validate: L.bind(this.validate, this, section_id)
1867 return widget.render();
1871 var CBIDummyValue = CBIValue.extend({
1872 __name__: 'CBI.DummyValue',
1874 renderWidget: function(section_id, option_index, cfgvalue) {
1875 var value = (cfgvalue != null) ? cfgvalue : this.default,
1876 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1877 outputEl = E('div');
1880 outputEl.appendChild(E('a', { 'href': this.href }));
1882 dom.append(outputEl.lastChild || outputEl,
1883 this.rawhtml ? value : [ value ]);
1891 remove: function() {},
1892 write: function() {}
1895 var CBIButtonValue = CBIValue.extend({
1896 __name__: 'CBI.ButtonValue',
1898 renderWidget: function(section_id, option_index, cfgvalue) {
1899 var value = (cfgvalue != null) ? cfgvalue : this.default,
1900 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1901 outputEl = E('div'),
1902 btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1904 if (value !== false)
1905 dom.content(outputEl, [
1907 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1908 'click': ui.createHandlerFn(this, function(section_id, ev) {
1910 return this.onclick(ev, section_id);
1912 ev.currentTarget.parentNode.nextElementSibling.value = value;
1913 return this.map.save();
1918 dom.content(outputEl, ' - ');
1927 var CBIHiddenValue = CBIValue.extend({
1928 __name__: 'CBI.HiddenValue',
1930 renderWidget: function(section_id, option_index, cfgvalue) {
1931 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1932 id: this.cbid(section_id)
1935 return widget.render();
1939 var CBIFileUpload = CBIValue.extend({
1940 __name__: 'CBI.FileSelect',
1942 __init__: function(/* ... */) {
1943 this.super('__init__', arguments);
1945 this.show_hidden = false;
1946 this.enable_upload = true;
1947 this.enable_remove = true;
1948 this.root_directory = '/etc/luci-uploads';
1951 renderWidget: function(section_id, option_index, cfgvalue) {
1952 var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
1953 id: this.cbid(section_id),
1954 name: this.cbid(section_id),
1955 show_hidden: this.show_hidden,
1956 enable_upload: this.enable_upload,
1957 enable_remove: this.enable_remove,
1958 root_directory: this.root_directory
1961 return browserEl.render();
1965 var CBISectionValue = CBIValue.extend({
1966 __name__: 'CBI.ContainerValue',
1967 __init__: function(map, section, option, cbiClass /*, ... */) {
1968 this.super('__init__', [map, section, option]);
1970 if (!CBIAbstractSection.isSubclass(cbiClass))
1971 throw 'Sub section must be a descendent of CBIAbstractSection';
1973 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1974 this.subsection.parentoption = this;
1977 load: function(section_id) {
1978 return this.subsection.load();
1981 parse: function(section_id) {
1982 return this.subsection.parse();
1985 renderWidget: function(section_id, option_index, cfgvalue) {
1986 return this.subsection.render();
1989 checkDepends: function(section_id) {
1990 this.subsection.checkDepends();
1991 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1994 write: function() {},
1995 remove: function() {},
1996 cfgvalue: function() { return null },
1997 formvalue: function() { return null }
2000 return baseclass.extend({
2002 JSONMap: CBIJSONMap,
2003 AbstractSection: CBIAbstractSection,
2004 AbstractValue: CBIAbstractValue,
2006 TypedSection: CBITypedSection,
2007 TableSection: CBITableSection,
2008 GridSection: CBIGridSection,
2009 NamedSection: CBINamedSection,
2012 DynamicList: CBIDynamicList,
2013 ListValue: CBIListValue,
2015 MultiValue: CBIMultiValue,
2016 TextValue: CBITextValue,
2017 DummyValue: CBIDummyValue,
2018 Button: CBIButtonValue,
2019 HiddenValue: CBIHiddenValue,
2020 FileUpload: CBIFileUpload,
2021 SectionValue: CBISectionValue