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) {
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++) {
1118 if (opt.optional || opt.modalonly)
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++) {
1132 if (opt.optional || opt.modalonly)
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)
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++) {
1168 if (opt.optional || opt.modalonly)
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 renderRowActions: function(section_id) {
1483 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1487 var section_ids = this.cfgsections(),
1490 if (Array.isArray(this.children)) {
1491 for (var i = 0; i < section_ids.length; i++) {
1492 for (var j = 0; j < this.children.length; j++) {
1493 if (!this.children[j].editable || this.children[j].modalonly)
1496 tasks.push(this.children[j].parse(section_ids[i]));
1501 return Promise.all(tasks);
1505 var CBINamedSection = CBIAbstractSection.extend({
1506 __name__: 'CBI.NamedSection',
1507 __init__: function(map, section_id /*, ... */) {
1508 this.super('__init__', this.varargs(arguments, 2, map));
1510 this.section = section_id;
1513 cfgsections: function() {
1514 return [ this.section ];
1517 handleAdd: function(ev) {
1518 var section_id = this.section,
1519 config_name = this.uciconfig || this.map.config;
1521 this.map.data.add(config_name, this.sectiontype, section_id);
1522 return this.map.save(null, true);
1525 handleRemove: function(ev) {
1526 var section_id = this.section,
1527 config_name = this.uciconfig || this.map.config;
1529 this.map.data.remove(config_name, section_id);
1530 return this.map.save(null, true);
1533 renderContents: function(data) {
1534 var ucidata = data[0], nodes = data[1],
1535 section_id = this.section,
1536 config_name = this.uciconfig || this.map.config,
1537 sectionEl = E('div', {
1538 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1539 'class': 'cbi-section',
1540 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
1541 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
1544 if (typeof(this.title) === 'string' && this.title !== '')
1545 sectionEl.appendChild(E('legend', {}, this.title));
1547 if (typeof(this.description) === 'string' && this.description !== '')
1548 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1551 if (this.addremove) {
1552 sectionEl.appendChild(
1553 E('div', { 'class': 'cbi-section-remove right' },
1555 'class': 'cbi-button',
1556 'click': ui.createHandlerFn(this, 'handleRemove')
1557 }, [ _('Delete') ])));
1560 sectionEl.appendChild(E('div', {
1561 'id': 'cbi-%s-%s'.format(config_name, section_id),
1563 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1564 'data-section-id': section_id
1567 else if (this.addremove) {
1568 sectionEl.appendChild(
1570 'class': 'cbi-button cbi-button-add',
1571 'click': ui.createHandlerFn(this, 'handleAdd')
1575 dom.bindClassInstance(sectionEl, this);
1580 render: function() {
1581 var config_name = this.uciconfig || this.map.config,
1582 section_id = this.section;
1584 return Promise.all([
1585 this.map.data.get(config_name, section_id),
1586 this.renderUCISection(section_id)
1587 ]).then(this.renderContents.bind(this));
1591 var CBIValue = CBIAbstractValue.extend({
1592 __name__: 'CBI.Value',
1594 value: function(key, val) {
1595 this.keylist = this.keylist || [];
1596 this.keylist.push(String(key));
1598 this.vallist = this.vallist || [];
1599 this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
1602 render: function(option_index, section_id, in_table) {
1603 return Promise.resolve(this.cfgvalue(section_id))
1604 .then(this.renderWidget.bind(this, section_id, option_index))
1605 .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1608 renderFrame: function(section_id, in_table, option_index, nodes) {
1609 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1610 depend_list = this.transformDepList(section_id),
1614 var title = this.stripTags(this.title).trim();
1615 optionEl = E('div', {
1616 'class': 'td cbi-value-field',
1617 'data-title': (title != '') ? title : null,
1618 'data-description': this.stripTags(this.description).trim(),
1619 'data-name': this.option,
1620 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1622 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1623 'data-index': option_index,
1624 'data-depends': depend_list,
1625 'data-field': this.cbid(section_id)
1629 optionEl = E('div', {
1630 'class': 'cbi-value',
1631 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1632 'data-index': option_index,
1633 'data-depends': depend_list,
1634 'data-field': this.cbid(section_id),
1635 'data-name': this.option,
1636 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1639 if (this.last_child)
1640 optionEl.classList.add('cbi-value-last');
1642 if (typeof(this.title) === 'string' && this.title !== '') {
1643 optionEl.appendChild(E('label', {
1644 'class': 'cbi-value-title',
1645 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
1646 'click': function(ev) {
1647 var node = ev.currentTarget,
1648 elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
1656 this.titleref ? E('a', {
1657 'class': 'cbi-title-ref',
1658 'href': this.titleref,
1659 'title': this.titledesc || _('Go to relevant configuration page')
1660 }, this.title) : this.title));
1662 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1667 (optionEl.lastChild || optionEl).appendChild(nodes);
1669 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1670 dom.append(optionEl.lastChild || optionEl,
1671 E('div', { 'class': 'cbi-value-description' }, this.description));
1673 if (depend_list && depend_list.length)
1674 optionEl.classList.add('hidden');
1676 optionEl.addEventListener('widget-change',
1677 L.bind(this.map.checkDepends, this.map));
1679 dom.bindClassInstance(optionEl, this);
1684 renderWidget: function(section_id, option_index, cfgvalue) {
1685 var value = (cfgvalue != null) ? cfgvalue : this.default,
1686 choices = this.transformChoices(),
1690 var placeholder = (this.optional || this.rmempty)
1691 ? E('em', _('unspecified')) : _('-- Please choose --');
1693 widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1694 id: this.cbid(section_id),
1696 optional: this.optional || this.rmempty,
1697 datatype: this.datatype,
1698 select_placeholder: this.placeholder || placeholder,
1699 validate: L.bind(this.validate, this, section_id)
1703 widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1704 id: this.cbid(section_id),
1705 password: this.password,
1706 optional: this.optional || this.rmempty,
1707 datatype: this.datatype,
1708 placeholder: this.placeholder,
1709 validate: L.bind(this.validate, this, section_id)
1713 return widget.render();
1717 var CBIDynamicList = CBIValue.extend({
1718 __name__: 'CBI.DynamicList',
1720 renderWidget: function(section_id, option_index, cfgvalue) {
1721 var value = (cfgvalue != null) ? cfgvalue : this.default,
1722 choices = this.transformChoices(),
1723 items = L.toArray(value);
1725 var widget = new ui.DynamicList(items, choices, {
1726 id: this.cbid(section_id),
1728 optional: this.optional || this.rmempty,
1729 datatype: this.datatype,
1730 placeholder: this.placeholder,
1731 validate: L.bind(this.validate, this, section_id)
1734 return widget.render();
1738 var CBIListValue = CBIValue.extend({
1739 __name__: 'CBI.ListValue',
1741 __init__: function() {
1742 this.super('__init__', arguments);
1743 this.widget = 'select';
1747 renderWidget: function(section_id, option_index, cfgvalue) {
1748 var choices = this.transformChoices();
1749 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1750 id: this.cbid(section_id),
1753 optional: this.optional,
1754 placeholder: this.placeholder,
1755 validate: L.bind(this.validate, this, section_id)
1758 return widget.render();
1762 var CBIFlagValue = CBIValue.extend({
1763 __name__: 'CBI.FlagValue',
1765 __init__: function() {
1766 this.super('__init__', arguments);
1769 this.disabled = '0';
1770 this.default = this.disabled;
1773 renderWidget: function(section_id, option_index, cfgvalue) {
1774 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1775 id: this.cbid(section_id),
1776 value_enabled: this.enabled,
1777 value_disabled: this.disabled,
1778 validate: L.bind(this.validate, this, section_id)
1781 return widget.render();
1784 formvalue: function(section_id) {
1785 var elem = this.getUIElement(section_id),
1786 checked = elem ? elem.isChecked() : false;
1787 return checked ? this.enabled : this.disabled;
1790 textvalue: function(section_id) {
1791 var cval = this.cfgvalue(section_id);
1794 cval = this.default;
1796 return (cval == this.enabled) ? _('Yes') : _('No');
1799 parse: function(section_id) {
1800 if (this.isActive(section_id)) {
1801 var fval = this.formvalue(section_id);
1803 if (!this.isValid(section_id))
1804 return Promise.reject();
1806 if (fval == this.default && (this.optional || this.rmempty))
1807 return Promise.resolve(this.remove(section_id));
1809 return Promise.resolve(this.write(section_id, fval));
1812 return Promise.resolve(this.remove(section_id));
1817 var CBIMultiValue = CBIDynamicList.extend({
1818 __name__: 'CBI.MultiValue',
1820 __init__: function() {
1821 this.super('__init__', arguments);
1822 this.placeholder = _('-- Please choose --');
1825 renderWidget: function(section_id, option_index, cfgvalue) {
1826 var value = (cfgvalue != null) ? cfgvalue : this.default,
1827 choices = this.transformChoices();
1829 var widget = new ui.Dropdown(L.toArray(value), choices, {
1830 id: this.cbid(section_id),
1833 optional: this.optional || this.rmempty,
1834 select_placeholder: this.placeholder,
1835 display_items: this.display_size || this.size || 3,
1836 dropdown_items: this.dropdown_size || this.size || -1,
1837 validate: L.bind(this.validate, this, section_id)
1840 return widget.render();
1844 var CBITextValue = CBIValue.extend({
1845 __name__: 'CBI.TextValue',
1849 renderWidget: function(section_id, option_index, cfgvalue) {
1850 var value = (cfgvalue != null) ? cfgvalue : this.default;
1852 var widget = new ui.Textarea(value, {
1853 id: this.cbid(section_id),
1854 optional: this.optional || this.rmempty,
1855 placeholder: this.placeholder,
1856 monospace: this.monospace,
1860 validate: L.bind(this.validate, this, section_id)
1863 return widget.render();
1867 var CBIDummyValue = CBIValue.extend({
1868 __name__: 'CBI.DummyValue',
1870 renderWidget: function(section_id, option_index, cfgvalue) {
1871 var value = (cfgvalue != null) ? cfgvalue : this.default,
1872 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1873 outputEl = E('div');
1876 outputEl.appendChild(E('a', { 'href': this.href }));
1878 dom.append(outputEl.lastChild || outputEl,
1879 this.rawhtml ? value : [ value ]);
1887 remove: function() {},
1888 write: function() {}
1891 var CBIButtonValue = CBIValue.extend({
1892 __name__: 'CBI.ButtonValue',
1894 renderWidget: function(section_id, option_index, cfgvalue) {
1895 var value = (cfgvalue != null) ? cfgvalue : this.default,
1896 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1897 outputEl = E('div'),
1898 btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1900 if (value !== false)
1901 dom.content(outputEl, [
1903 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1904 'click': ui.createHandlerFn(this, function(section_id, ev) {
1906 return this.onclick(ev, section_id);
1908 ev.currentTarget.parentNode.nextElementSibling.value = value;
1909 return this.map.save();
1914 dom.content(outputEl, ' - ');
1923 var CBIHiddenValue = CBIValue.extend({
1924 __name__: 'CBI.HiddenValue',
1926 renderWidget: function(section_id, option_index, cfgvalue) {
1927 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1928 id: this.cbid(section_id)
1931 return widget.render();
1935 var CBIFileUpload = CBIValue.extend({
1936 __name__: 'CBI.FileSelect',
1938 __init__: function(/* ... */) {
1939 this.super('__init__', arguments);
1941 this.show_hidden = false;
1942 this.enable_upload = true;
1943 this.enable_remove = true;
1944 this.root_directory = '/etc/luci-uploads';
1947 renderWidget: function(section_id, option_index, cfgvalue) {
1948 var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
1949 id: this.cbid(section_id),
1950 name: this.cbid(section_id),
1951 show_hidden: this.show_hidden,
1952 enable_upload: this.enable_upload,
1953 enable_remove: this.enable_remove,
1954 root_directory: this.root_directory
1957 return browserEl.render();
1961 var CBISectionValue = CBIValue.extend({
1962 __name__: 'CBI.ContainerValue',
1963 __init__: function(map, section, option, cbiClass /*, ... */) {
1964 this.super('__init__', [map, section, option]);
1966 if (!CBIAbstractSection.isSubclass(cbiClass))
1967 throw 'Sub section must be a descendent of CBIAbstractSection';
1969 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1970 this.subsection.parentoption = this;
1973 load: function(section_id) {
1974 return this.subsection.load();
1977 parse: function(section_id) {
1978 return this.subsection.parse();
1981 renderWidget: function(section_id, option_index, cfgvalue) {
1982 return this.subsection.render();
1985 checkDepends: function(section_id) {
1986 this.subsection.checkDepends();
1987 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1990 write: function() {},
1991 remove: function() {},
1992 cfgvalue: function() { return null },
1993 formvalue: function() { return null }
1996 return baseclass.extend({
1998 JSONMap: CBIJSONMap,
1999 AbstractSection: CBIAbstractSection,
2000 AbstractValue: CBIAbstractValue,
2002 TypedSection: CBITypedSection,
2003 TableSection: CBITableSection,
2004 GridSection: CBIGridSection,
2005 NamedSection: CBINamedSection,
2008 DynamicList: CBIDynamicList,
2009 ListValue: CBIListValue,
2011 MultiValue: CBIMultiValue,
2012 TextValue: CBITextValue,
2013 DummyValue: CBIDummyValue,
2014 Button: CBIButtonValue,
2015 HiddenValue: CBIHiddenValue,
2016 FileUpload: CBIFileUpload,
2017 SectionValue: CBISectionValue