Merge pull request #1735 from sumpfralle/olsr-jsoninfo-parser-handle-empty-result
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / form.js
1 'use strict';
2 'require ui';
3 'require uci';
4
5 var scope = this;
6
7 var CBIJSONConfig = Class.extend({
8         __init__: function(data) {
9                 data = Object.assign({}, data);
10
11                 this.data = {};
12
13                 var num_sections = 0,
14                     section_ids = [];
15
16                 for (var sectiontype in data) {
17                         if (!data.hasOwnProperty(sectiontype))
18                                 continue;
19
20                         if (L.isObject(data[sectiontype])) {
21                                 this.data[sectiontype] = Object.assign(data[sectiontype], {
22                                         '.anonymous': false,
23                                         '.name': sectiontype,
24                                         '.type': sectiontype
25                                 });
26
27                                 section_ids.push(sectiontype);
28                                 num_sections++;
29                         }
30                         else if (Array.isArray(data[sectiontype])) {
31                                 for (var i = 0, index = 0; i < data[sectiontype].length; i++) {
32                                         var item = data[sectiontype][i],
33                                             anonymous, name;
34
35                                         if (!L.isObject(item))
36                                                 continue;
37
38                                         if (typeof(item['.name']) == 'string') {
39                                                 name = item['.name'];
40                                                 anonymous = false;
41                                         }
42                                         else {
43                                                 name = sectiontype + num_sections;
44                                                 anonymous = true;
45                                         }
46
47                                         if (!this.data.hasOwnProperty(name))
48                                                 section_ids.push(name);
49
50                                         this.data[name] = Object.assign(item, {
51                                                 '.index': num_sections++,
52                                                 '.anonymous': anonymous,
53                                                 '.name': name,
54                                                 '.type': sectiontype
55                                         });
56                                 }
57                         }
58                 }
59
60                 section_ids.sort(L.bind(function(a, b) {
61                         var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999,
62                             indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
63
64                         if (indexA != indexB)
65                                 return (indexA - indexB);
66
67                         return (a > b);
68                 }, this));
69
70                 for (var i = 0; i < section_ids.length; i++)
71                         this.data[section_ids[i]]['.index'] = i;
72         },
73
74         load: function() {
75                 return Promise.resolve(this.data);
76         },
77
78         save: function() {
79                 return Promise.resolve();
80         },
81
82         get: function(config, section, option) {
83                 if (section == null)
84                         return null;
85
86                 if (option == null)
87                         return this.data[section];
88
89                 if (!this.data.hasOwnProperty(section))
90                         return null;
91
92                 var value = this.data[section][option];
93
94                 if (Array.isArray(value))
95                         return value;
96
97                 if (value != null)
98                         return String(value);
99
100                 return null;
101         },
102
103         set: function(config, section, option, value) {
104                 if (section == null || option == null || option.charAt(0) == '.')
105                         return;
106
107                 if (!this.data.hasOwnProperty(section))
108                         return;
109
110                 if (value == null)
111                         delete this.data[section][option];
112                 else if (Array.isArray(value))
113                         this.data[section][option] = value;
114                 else
115                         this.data[section][option] = String(value);
116         },
117
118         unset: function(config, section, option) {
119                 return this.set(config, section, option, null);
120         },
121
122         sections: function(config, sectiontype, callback) {
123                 var rv = [];
124
125                 for (var section_id in this.data)
126                         if (sectiontype == null || this.data[section_id]['.type'] == sectiontype)
127                                 rv.push(this.data[section_id]);
128
129                 rv.sort(function(a, b) { return a['.index'] - b['.index'] });
130
131                 if (typeof(callback) == 'function')
132                         for (var i = 0; i < rv.length; i++)
133                                 callback.call(this, rv[i], rv[i]['.name']);
134
135                 return rv;
136         },
137
138         add: function(config, sectiontype, sectionname) {
139                 var num_sections_type = 0, next_index = 0;
140
141                 for (var name in this.data) {
142                         num_sections_type += (this.data[name]['.type'] == sectiontype);
143                         next_index = Math.max(next_index, this.data[name]['.index']);
144                 }
145
146                 var section_id = sectionname || sectiontype + num_sections_type;
147
148                 if (!this.data.hasOwnProperty(section_id)) {
149                         this.data[section_id] = {
150                                 '.name': section_id,
151                                 '.type': sectiontype,
152                                 '.anonymous': (sectionname == null),
153                                 '.index': next_index + 1
154                         };
155                 }
156
157                 return section_id;
158         },
159
160         remove: function(config, section) {
161                 if (this.data.hasOwnProperty(section))
162                         delete this.data[section];
163         },
164
165         resolveSID: function(config, section_id) {
166                 return section_id;
167         },
168
169         move: function(config, section_id1, section_id2, after) {
170                 return uci.move.apply(this, [config, section_id1, section_id2, after]);
171         }
172 });
173
174 var CBINode = Class.extend({
175         __init__: function(title, description) {
176                 this.title = title || '';
177                 this.description = description || '';
178                 this.children = [];
179         },
180
181         append: function(obj) {
182                 this.children.push(obj);
183         },
184
185         parse: function() {
186                 var args = arguments;
187                 this.children.forEach(function(child) {
188                         child.parse.apply(child, args);
189                 });
190         },
191
192         render: function() {
193                 L.error('InternalError', 'Not implemented');
194         },
195
196         loadChildren: function(/* ... */) {
197                 var tasks = [];
198
199                 if (Array.isArray(this.children))
200                         for (var i = 0; i < this.children.length; i++)
201                                 if (!this.children[i].disable)
202                                         tasks.push(this.children[i].load.apply(this.children[i], arguments));
203
204                 return Promise.all(tasks);
205         },
206
207         renderChildren: function(tab_name /*, ... */) {
208                 var tasks = [],
209                     index = 0;
210
211                 if (Array.isArray(this.children))
212                         for (var i = 0; i < this.children.length; i++)
213                                 if (tab_name === null || this.children[i].tab === tab_name)
214                                         if (!this.children[i].disable)
215                                                 tasks.push(this.children[i].render.apply(
216                                                         this.children[i], this.varargs(arguments, 1, index++)));
217
218                 return Promise.all(tasks);
219         },
220
221         stripTags: function(s) {
222                 if (typeof(s) == 'string' && !s.match(/[<>]/))
223                         return s;
224
225                 var x = E('div', {}, s);
226                 return x.textContent || x.innerText || '';
227         },
228
229         titleFn: function(attr /*, ... */) {
230                 var s = null;
231
232                 if (typeof(this[attr]) == 'function')
233                         s = this[attr].apply(this, this.varargs(arguments, 1));
234                 else if (typeof(this[attr]) == 'string')
235                         s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
236
237                 if (s != null)
238                         s = this.stripTags(String(s)).trim();
239
240                 if (s == null || s == '')
241                         return null;
242
243                 return s;
244         }
245 });
246
247 var CBIMap = CBINode.extend({
248         __init__: function(config /*, ... */) {
249                 this.super('__init__', this.varargs(arguments, 1));
250
251                 this.config = config;
252                 this.parsechain = [ config ];
253                 this.data = uci;
254         },
255
256         findElements: function(/* ... */) {
257                 var q = null;
258
259                 if (arguments.length == 1)
260                         q = arguments[0];
261                 else if (arguments.length == 2)
262                         q = '[%s="%s"]'.format(arguments[0], arguments[1]);
263                 else
264                         L.error('InternalError', 'Expecting one or two arguments to findElements()');
265
266                 return this.root.querySelectorAll(q);
267         },
268
269         findElement: function(/* ... */) {
270                 var res = this.findElements.apply(this, arguments);
271                 return res.length ? res[0] : null;
272         },
273
274         chain: function(config) {
275                 if (this.parsechain.indexOf(config) == -1)
276                         this.parsechain.push(config);
277         },
278
279         section: function(cbiClass /*, ... */) {
280                 if (!CBIAbstractSection.isSubclass(cbiClass))
281                         L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
282
283                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
284                 this.append(obj);
285                 return obj;
286         },
287
288         load: function() {
289                 return this.data.load(this.parsechain || [ this.config ])
290                         .then(this.loadChildren.bind(this));
291         },
292
293         parse: function() {
294                 var tasks = [];
295
296                 if (Array.isArray(this.children))
297                         for (var i = 0; i < this.children.length; i++)
298                                 tasks.push(this.children[i].parse());
299
300                 return Promise.all(tasks);
301         },
302
303         save: function(cb, silent) {
304                 this.checkDepends();
305
306                 return this.parse()
307                         .then(cb)
308                         .then(this.data.save.bind(this.data))
309                         .then(this.load.bind(this))
310                         .catch(function(e) {
311                                 if (!silent)
312                                         alert('Cannot save due to invalid values');
313
314                                 return Promise.reject();
315                         }).finally(this.renderContents.bind(this));
316         },
317
318         reset: function() {
319                 return this.renderContents();
320         },
321
322         render: function() {
323                 return this.load().then(this.renderContents.bind(this));
324         },
325
326         renderContents: function() {
327                 var mapEl = this.root || (this.root = E('div', {
328                         'id': 'cbi-%s'.format(this.config),
329                         'class': 'cbi-map',
330                         'cbi-dependency-check': L.bind(this.checkDepends, this)
331                 }));
332
333                 L.dom.bindClassInstance(mapEl, this);
334
335                 return this.renderChildren(null).then(L.bind(function(nodes) {
336                         var initialRender = !mapEl.firstChild;
337
338                         L.dom.content(mapEl, null);
339
340                         if (this.title != null && this.title != '')
341                                 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
342
343                         if (this.description != null && this.description != '')
344                                 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
345
346                         if (this.tabbed)
347                                 L.dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
348                         else
349                                 L.dom.append(mapEl, nodes);
350
351                         if (!initialRender) {
352                                 mapEl.classList.remove('flash');
353
354                                 window.setTimeout(function() {
355                                         mapEl.classList.add('flash');
356                                 }, 1);
357                         }
358
359                         this.checkDepends();
360
361                         var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
362
363                         for (var i = 0; i < tabGroups.length; i++)
364                                 ui.tabs.initTabGroup(tabGroups[i].childNodes);
365
366                         return mapEl;
367                 }, this));
368         },
369
370         lookupOption: function(name, section_id, config_name) {
371                 var id, elem, sid, inst;
372
373                 if (name.indexOf('.') > -1)
374                         id = 'cbid.%s'.format(name);
375                 else
376                         id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
377
378                 elem = this.findElement('data-field', id);
379                 sid  = elem ? id.split(/\./)[2] : null;
380                 inst = elem ? L.dom.findClassInstance(elem) : null;
381
382                 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
383         },
384
385         checkDepends: function(ev, n) {
386                 var changed = false;
387
388                 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
389                         if (s.checkDepends(ev, n))
390                                 changed = true;
391
392                 if (changed && (n || 0) < 10)
393                         this.checkDepends(ev, (n || 10) + 1);
394
395                 ui.tabs.updateTabs(ev, this.root);
396         },
397
398         isDependencySatisfied: function(depends, config_name, section_id) {
399                 var def = false;
400
401                 if (!Array.isArray(depends) || !depends.length)
402                         return true;
403
404                 for (var i = 0; i < depends.length; i++) {
405                         var istat = true,
406                             reverse = false;
407
408                         for (var dep in depends[i]) {
409                                 if (dep == '!reverse') {
410                                         reverse = true;
411                                 }
412                                 else if (dep == '!default') {
413                                         def = true;
414                                         istat = false;
415                                 }
416                                 else {
417                                         var res = this.lookupOption(dep, section_id, config_name),
418                                             val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
419
420                                         istat = (istat && isEqual(val, depends[i][dep]));
421                                 }
422                         }
423
424                         if (istat ^ reverse)
425                                 return true;
426                 }
427
428                 return def;
429         }
430 });
431
432 var CBIJSONMap = CBIMap.extend({
433         __init__: function(data /*, ... */) {
434                 this.super('__init__', this.varargs(arguments, 1, 'json'));
435
436                 this.config = 'json';
437                 this.parsechain = [ 'json' ];
438                 this.data = new CBIJSONConfig(data);
439         }
440 });
441
442 var CBIAbstractSection = CBINode.extend({
443         __init__: function(map, sectionType /*, ... */) {
444                 this.super('__init__', this.varargs(arguments, 2));
445
446                 this.sectiontype = sectionType;
447                 this.map = map;
448                 this.config = map.config;
449
450                 this.optional = true;
451                 this.addremove = false;
452                 this.dynamic = false;
453         },
454
455         cfgsections: function() {
456                 L.error('InternalError', 'Not implemented');
457         },
458
459         filter: function(section_id) {
460                 return true;
461         },
462
463         load: function() {
464                 var section_ids = this.cfgsections(),
465                     tasks = [];
466
467                 if (Array.isArray(this.children))
468                         for (var i = 0; i < section_ids.length; i++)
469                                 tasks.push(this.loadChildren(section_ids[i])
470                                         .then(Function.prototype.bind.call(function(section_id, set_values) {
471                                                 for (var i = 0; i < set_values.length; i++)
472                                                         this.children[i].cfgvalue(section_id, set_values[i]);
473                                         }, this, section_ids[i])));
474
475                 return Promise.all(tasks);
476         },
477
478         parse: function() {
479                 var section_ids = this.cfgsections(),
480                     tasks = [];
481
482                 if (Array.isArray(this.children))
483                         for (var i = 0; i < section_ids.length; i++)
484                                 for (var j = 0; j < this.children.length; j++)
485                                         tasks.push(this.children[j].parse(section_ids[i]));
486
487                 return Promise.all(tasks);
488         },
489
490         tab: function(name, title, description) {
491                 if (this.tabs && this.tabs[name])
492                         throw 'Tab already declared';
493
494                 var entry = {
495                         name: name,
496                         title: title,
497                         description: description,
498                         children: []
499                 };
500
501                 this.tabs = this.tabs || [];
502                 this.tabs.push(entry);
503                 this.tabs[name] = entry;
504
505                 this.tab_names = this.tab_names || [];
506                 this.tab_names.push(name);
507         },
508
509         option: function(cbiClass /*, ... */) {
510                 if (!CBIAbstractValue.isSubclass(cbiClass))
511                         throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
512
513                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
514                 this.append(obj);
515                 return obj;
516         },
517
518         taboption: function(tabName /*, ... */) {
519                 if (!this.tabs || !this.tabs[tabName])
520                         throw L.error('ReferenceError', 'Associated tab not declared');
521
522                 var obj = this.option.apply(this, this.varargs(arguments, 1));
523                 obj.tab = tabName;
524                 this.tabs[tabName].children.push(obj);
525                 return obj;
526         },
527
528         renderUCISection: function(section_id) {
529                 var renderTasks = [];
530
531                 if (!this.tabs)
532                         return this.renderOptions(null, section_id);
533
534                 for (var i = 0; i < this.tab_names.length; i++)
535                         renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
536
537                 return Promise.all(renderTasks)
538                         .then(this.renderTabContainers.bind(this, section_id));
539         },
540
541         renderTabContainers: function(section_id, nodes) {
542                 var config_name = this.uciconfig || this.map.config,
543                     containerEls = E([]);
544
545                 for (var i = 0; i < nodes.length; i++) {
546                         var tab_name = this.tab_names[i],
547                             tab_data = this.tabs[tab_name],
548                             containerEl = E('div', {
549                                 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
550                                 'data-tab': tab_name,
551                                 'data-tab-title': tab_data.title,
552                                 'data-tab-active': tab_name === this.selected_tab
553                             });
554
555                         if (tab_data.description != null && tab_data.description != '')
556                                 containerEl.appendChild(
557                                         E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
558
559                         containerEl.appendChild(nodes[i]);
560                         containerEls.appendChild(containerEl);
561                 }
562
563                 return containerEls;
564         },
565
566         renderOptions: function(tab_name, section_id) {
567                 var in_table = (this instanceof CBITableSection);
568                 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
569                         var optionEls = E([]);
570                         for (var i = 0; i < nodes.length; i++)
571                                 optionEls.appendChild(nodes[i]);
572                         return optionEls;
573                 });
574         },
575
576         checkDepends: function(ev, n) {
577                 var changed = false,
578                     sids = this.cfgsections();
579
580                 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
581                         for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
582                                 var isActive = o.isActive(sid),
583                                     isSatisified = o.checkDepends(sid);
584
585                                 if (isActive != isSatisified) {
586                                         o.setActive(sid, !isActive);
587                                         changed = true;
588                                 }
589
590                                 if (!n && isActive)
591                                         o.triggerValidation(sid);
592                         }
593                 }
594
595                 return changed;
596         }
597 });
598
599
600 var isEqual = function(x, y) {
601         if (x != null && y != null && typeof(x) != typeof(y))
602                 return false;
603
604         if ((x == null && y != null) || (x != null && y == null))
605                 return false;
606
607         if (Array.isArray(x)) {
608                 if (x.length != y.length)
609                         return false;
610
611                 for (var i = 0; i < x.length; i++)
612                         if (!isEqual(x[i], y[i]))
613                                 return false;
614         }
615         else if (typeof(x) == 'object') {
616                 for (var k in x) {
617                         if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
618                                 return false;
619
620                         if (!isEqual(x[k], y[k]))
621                                 return false;
622                 }
623
624                 for (var k in y)
625                         if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
626                                 return false;
627         }
628         else if (x != y) {
629                 return false;
630         }
631
632         return true;
633 };
634
635 var CBIAbstractValue = CBINode.extend({
636         __init__: function(map, section, option /*, ... */) {
637                 this.super('__init__', this.varargs(arguments, 3));
638
639                 this.section = section;
640                 this.option = option;
641                 this.map = map;
642                 this.config = map.config;
643
644                 this.deps = [];
645                 this.initial = {};
646                 this.rmempty = true;
647                 this.default = null;
648                 this.size = null;
649                 this.optional = false;
650         },
651
652         depends: function(field, value) {
653                 var deps;
654
655                 if (typeof(field) === 'string')
656                         deps = {}, deps[field] = value;
657                 else
658                         deps = field;
659
660                 this.deps.push(deps);
661         },
662
663         transformDepList: function(section_id, deplist) {
664                 var list = deplist || this.deps,
665                     deps = [];
666
667                 if (Array.isArray(list)) {
668                         for (var i = 0; i < list.length; i++) {
669                                 var dep = {};
670
671                                 for (var k in list[i]) {
672                                         if (list[i].hasOwnProperty(k)) {
673                                                 if (k.charAt(0) === '!')
674                                                         dep[k] = list[i][k];
675                                                 else if (k.indexOf('.') !== -1)
676                                                         dep['cbid.%s'.format(k)] = list[i][k];
677                                                 else
678                                                         dep['cbid.%s.%s.%s'.format(
679                                                                 this.uciconfig || this.section.uciconfig || this.map.config,
680                                                                 this.ucisection || section_id,
681                                                                 k
682                                                         )] = list[i][k];
683                                         }
684                                 }
685
686                                 for (var k in dep) {
687                                         if (dep.hasOwnProperty(k)) {
688                                                 deps.push(dep);
689                                                 break;
690                                         }
691                                 }
692                         }
693                 }
694
695                 return deps;
696         },
697
698         transformChoices: function() {
699                 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
700                         return null;
701
702                 var choices = {};
703
704                 for (var i = 0; i < this.keylist.length; i++)
705                         choices[this.keylist[i]] = this.vallist[i];
706
707                 return choices;
708         },
709
710         checkDepends: function(section_id) {
711                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
712                     active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
713
714                 if (active)
715                         this.updateDefaultValue(section_id);
716
717                 return active;
718         },
719
720         updateDefaultValue: function(section_id) {
721                 if (!L.isObject(this.defaults))
722                         return;
723
724                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
725                     cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
726                     default_defval = null, satisified_defval = null;
727
728                 for (var value in this.defaults) {
729                         if (!this.defaults[value] || this.defaults[value].length == 0) {
730                                 default_defval = value;
731                                 continue;
732                         }
733                         else if (this.map.isDependencySatisfied(this.defaults[value], config_name, section_id)) {
734                                 satisified_defval = value;
735                                 break;
736                         }
737                 }
738
739                 if (satisified_defval == null)
740                         satisified_defval = default_defval;
741
742                 var node = this.map.findElement('id', this.cbid(section_id));
743                 if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
744                         L.dom.callClassMethod(node, 'setValue', satisified_defval);
745
746                 this.default = satisified_defval;
747         },
748
749         cbid: function(section_id) {
750                 if (section_id == null)
751                         L.error('TypeError', 'Section ID required');
752
753                 return 'cbid.%s.%s.%s'.format(
754                         this.uciconfig || this.section.uciconfig || this.map.config,
755                         section_id, this.option);
756         },
757
758         load: function(section_id) {
759                 if (section_id == null)
760                         L.error('TypeError', 'Section ID required');
761
762                 return this.map.data.get(
763                         this.uciconfig || this.section.uciconfig || this.map.config,
764                         this.ucisection || section_id,
765                         this.ucioption || this.option);
766         },
767
768         cfgvalue: function(section_id, set_value) {
769                 if (section_id == null)
770                         L.error('TypeError', 'Section ID required');
771
772                 if (arguments.length == 2) {
773                         this.data = this.data || {};
774                         this.data[section_id] = set_value;
775                 }
776
777                 return this.data ? this.data[section_id] : null;
778         },
779
780         formvalue: function(section_id) {
781                 var node = this.map.findElement('id', this.cbid(section_id));
782                 return node ? L.dom.callClassMethod(node, 'getValue') : null;
783         },
784
785         textvalue: function(section_id) {
786                 var cval = this.cfgvalue(section_id);
787
788                 if (cval == null)
789                         cval = this.default;
790
791                 return (cval != null) ? '%h'.format(cval) : null;
792         },
793
794         validate: function(section_id, value) {
795                 return true;
796         },
797
798         isValid: function(section_id) {
799                 var node = this.map.findElement('id', this.cbid(section_id));
800                 return node ? L.dom.callClassMethod(node, 'isValid') : true;
801         },
802
803         isActive: function(section_id) {
804                 var field = this.map.findElement('data-field', this.cbid(section_id));
805                 return (field != null && !field.classList.contains('hidden'));
806         },
807
808         setActive: function(section_id, active) {
809                 var field = this.map.findElement('data-field', this.cbid(section_id));
810
811                 if (field && field.classList.contains('hidden') == active) {
812                         field.classList[active ? 'remove' : 'add']('hidden');
813                         return true;
814                 }
815
816                 return false;
817         },
818
819         triggerValidation: function(section_id) {
820                 var node = this.map.findElement('id', this.cbid(section_id));
821                 return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
822         },
823
824         parse: function(section_id) {
825                 var active = this.isActive(section_id),
826                     cval = this.cfgvalue(section_id),
827                     fval = active ? this.formvalue(section_id) : null;
828
829                 if (active && !this.isValid(section_id))
830                         return Promise.reject();
831
832                 if (fval != '' && fval != null) {
833                         if (this.forcewrite || !isEqual(cval, fval))
834                                 return Promise.resolve(this.write(section_id, fval));
835                 }
836                 else {
837                         if (this.rmempty || this.optional) {
838                                 return Promise.resolve(this.remove(section_id));
839                         }
840                         else if (!isEqual(cval, fval)) {
841                                 console.log('This should have been catched by isValid()');
842                                 return Promise.reject();
843                         }
844                 }
845
846                 return Promise.resolve();
847         },
848
849         write: function(section_id, formvalue) {
850                 return this.map.data.set(
851                         this.uciconfig || this.section.uciconfig || this.map.config,
852                         this.ucisection || section_id,
853                         this.ucioption || this.option,
854                         formvalue);
855         },
856
857         remove: function(section_id) {
858                 return this.map.data.unset(
859                         this.uciconfig || this.section.uciconfig || this.map.config,
860                         this.ucisection || section_id,
861                         this.ucioption || this.option);
862         }
863 });
864
865 var CBITypedSection = CBIAbstractSection.extend({
866         __name__: 'CBI.TypedSection',
867
868         cfgsections: function() {
869                 return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
870                         .map(function(s) { return s['.name'] })
871                         .filter(L.bind(this.filter, this));
872         },
873
874         handleAdd: function(ev, name) {
875                 var config_name = this.uciconfig || this.map.config;
876
877                 this.map.data.add(config_name, this.sectiontype, name);
878                 return this.map.save(null, true);
879         },
880
881         handleRemove: function(section_id, ev) {
882                 var config_name = this.uciconfig || this.map.config;
883
884                 this.map.data.remove(config_name, section_id);
885                 return this.map.save(null, true);
886         },
887
888         renderSectionAdd: function(extra_class) {
889                 if (!this.addremove)
890                         return E([]);
891
892                 var createEl = E('div', { 'class': 'cbi-section-create' }),
893                     config_name = this.uciconfig || this.map.config,
894                     btn_title = this.titleFn('addbtntitle');
895
896                 if (extra_class != null)
897                         createEl.classList.add(extra_class);
898
899                 if (this.anonymous) {
900                         createEl.appendChild(E('button', {
901                                 'class': 'cbi-button cbi-button-add',
902                                 'title': btn_title || _('Add'),
903                                 'click': L.ui.createHandlerFn(this, 'handleAdd')
904                         }, [ btn_title || _('Add') ]));
905                 }
906                 else {
907                         var nameEl = E('input', {
908                                 'type': 'text',
909                                 'class': 'cbi-section-create-name'
910                         });
911
912                         L.dom.append(createEl, [
913                                 E('div', {}, nameEl),
914                                 E('input', {
915                                         'class': 'cbi-button cbi-button-add',
916                                         'type': 'submit',
917                                         'value': btn_title || _('Add'),
918                                         'title': btn_title || _('Add'),
919                                         'click': L.ui.createHandlerFn(this, function(ev) {
920                                                 if (nameEl.classList.contains('cbi-input-invalid'))
921                                                         return;
922
923                                                 return this.handleAdd(ev, nameEl.value);
924                                         })
925                                 })
926                         ]);
927
928                         ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
929                 }
930
931                 return createEl;
932         },
933
934         renderSectionPlaceholder: function() {
935                 return E([
936                         E('em', _('This section contains no values yet')),
937                         E('br'), E('br')
938                 ]);
939         },
940
941         renderContents: function(cfgsections, nodes) {
942                 var section_id = null,
943                     config_name = this.uciconfig || this.map.config,
944                     sectionEl = E('div', {
945                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
946                                 'class': 'cbi-section',
947                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
948                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
949                         });
950
951                 if (this.title != null && this.title != '')
952                         sectionEl.appendChild(E('legend', {}, this.title));
953
954                 if (this.description != null && this.description != '')
955                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
956
957                 for (var i = 0; i < nodes.length; i++) {
958                         if (this.addremove) {
959                                 sectionEl.appendChild(
960                                         E('div', { 'class': 'cbi-section-remove right' },
961                                                 E('button', {
962                                                         'class': 'cbi-button',
963                                                         'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
964                                                         'data-section-id': cfgsections[i],
965                                                         'click': L.ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
966                                                 }, [ _('Delete') ])));
967                         }
968
969                         if (!this.anonymous)
970                                 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
971
972                         sectionEl.appendChild(E('div', {
973                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
974                                 'class': this.tabs
975                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
976                                 'data-section-id': cfgsections[i]
977                         }, nodes[i]));
978                 }
979
980                 if (nodes.length == 0)
981                         sectionEl.appendChild(this.renderSectionPlaceholder());
982
983                 sectionEl.appendChild(this.renderSectionAdd());
984
985                 L.dom.bindClassInstance(sectionEl, this);
986
987                 return sectionEl;
988         },
989
990         render: function() {
991                 var cfgsections = this.cfgsections(),
992                     renderTasks = [];
993
994                 for (var i = 0; i < cfgsections.length; i++)
995                         renderTasks.push(this.renderUCISection(cfgsections[i]));
996
997                 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
998         }
999 });
1000
1001 var CBITableSection = CBITypedSection.extend({
1002         __name__: 'CBI.TableSection',
1003
1004         tab: function() {
1005                 throw 'Tabs are not supported by TableSection';
1006         },
1007
1008         renderContents: function(cfgsections, nodes) {
1009                 var section_id = null,
1010                     config_name = this.uciconfig || this.map.config,
1011                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1012                     has_more = max_cols < this.children.length,
1013                     sectionEl = E('div', {
1014                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
1015                                 'class': 'cbi-section cbi-tblsection',
1016                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
1017                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
1018                         }),
1019                         tableEl = E('div', {
1020                                 'class': 'table cbi-section-table'
1021                         });
1022
1023                 if (this.title != null && this.title != '')
1024                         sectionEl.appendChild(E('h3', {}, this.title));
1025
1026                 if (this.description != null && this.description != '')
1027                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1028
1029                 tableEl.appendChild(this.renderHeaderRows(max_cols));
1030
1031                 for (var i = 0; i < nodes.length; i++) {
1032                         var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
1033
1034                         var trEl = E('div', {
1035                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
1036                                 'class': 'tr cbi-section-table-row',
1037                                 'data-sid': cfgsections[i],
1038                                 'draggable': this.sortable ? true : null,
1039                                 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
1040                                 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
1041                                 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
1042                                 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
1043                                 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
1044                                 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
1045                                 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
1046                                 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
1047                                 'data-section-id': cfgsections[i]
1048                         });
1049
1050                         if (this.extedit || this.rowcolors)
1051                                 trEl.classList.add(!(tableEl.childNodes.length % 2)
1052                                         ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
1053
1054                         for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
1055                                 trEl.appendChild(nodes[i].firstChild);
1056
1057                         trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
1058                         tableEl.appendChild(trEl);
1059                 }
1060
1061                 if (nodes.length == 0)
1062                         tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
1063                                 E('div', { 'class': 'td' },
1064                                         E('em', {}, _('This section contains no values yet')))));
1065
1066                 sectionEl.appendChild(tableEl);
1067
1068                 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
1069
1070                 L.dom.bindClassInstance(sectionEl, this);
1071
1072                 return sectionEl;
1073         },
1074
1075         renderHeaderRows: function(max_cols) {
1076                 var has_titles = false,
1077                     has_descriptions = false,
1078                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1079                     has_more = max_cols < this.children.length,
1080                     anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
1081                     trEls = E([]);
1082
1083                 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1084                         if (opt.optional || opt.modalonly)
1085                                 continue;
1086
1087                         has_titles = has_titles || !!opt.title;
1088                         has_descriptions = has_descriptions || !!opt.description;
1089                 }
1090
1091                 if (has_titles) {
1092                         var trEl = E('div', {
1093                                 'class': 'tr cbi-section-table-titles ' + anon_class,
1094                                 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
1095                         });
1096
1097                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1098                                 if (opt.optional || opt.modalonly)
1099                                         continue;
1100
1101                                 trEl.appendChild(E('div', {
1102                                         'class': 'th cbi-section-table-cell',
1103                                         'data-type': opt.__name__
1104                                 }));
1105
1106                                 if (opt.width != null)
1107                                         trEl.lastElementChild.style.width =
1108                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1109
1110                                 if (opt.titleref)
1111                                         trEl.lastElementChild.appendChild(E('a', {
1112                                                 'href': opt.titleref,
1113                                                 'class': 'cbi-title-ref',
1114                                                 'title': this.titledesc || _('Go to relevant configuration page')
1115                                         }, opt.title));
1116                                 else
1117                                         L.dom.content(trEl.lastElementChild, opt.title);
1118                         }
1119
1120                         if (this.sortable || this.extedit || this.addremove || has_more)
1121                                 trEl.appendChild(E('div', {
1122                                         'class': 'th cbi-section-table-cell cbi-section-actions'
1123                                 }));
1124
1125                         trEls.appendChild(trEl);
1126                 }
1127
1128                 if (has_descriptions) {
1129                         var trEl = E('div', {
1130                                 'class': 'tr cbi-section-table-descr ' + anon_class
1131                         });
1132
1133                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1134                                 if (opt.optional || opt.modalonly)
1135                                         continue;
1136
1137                                 trEl.appendChild(E('div', {
1138                                         'class': 'th cbi-section-table-cell',
1139                                         'data-type': opt.__name__
1140                                 }, opt.description));
1141
1142                                 if (opt.width != null)
1143                                         trEl.lastElementChild.style.width =
1144                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1145                         }
1146
1147                         if (this.sortable || this.extedit || this.addremove || has_more)
1148                                 trEl.appendChild(E('div', {
1149                                         'class': 'th cbi-section-table-cell cbi-section-actions'
1150                                 }));
1151
1152                         trEls.appendChild(trEl);
1153                 }
1154
1155                 return trEls;
1156         },
1157
1158         renderRowActions: function(section_id, more_label) {
1159                 var config_name = this.uciconfig || this.map.config;
1160
1161                 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
1162                         return E([]);
1163
1164                 var tdEl = E('div', {
1165                         'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
1166                 }, E('div'));
1167
1168                 if (this.sortable) {
1169                         L.dom.append(tdEl.lastElementChild, [
1170                                 E('div', {
1171                                         'title': _('Drag to reorder'),
1172                                         'class': 'cbi-button drag-handle center',
1173                                         'style': 'cursor:move'
1174                                 }, '☰')
1175                         ]);
1176                 }
1177
1178                 if (this.extedit) {
1179                         var evFn = null;
1180
1181                         if (typeof(this.extedit) == 'function')
1182                                 evFn = L.bind(this.extedit, this);
1183                         else if (typeof(this.extedit) == 'string')
1184                                 evFn = L.bind(function(sid, ev) {
1185                                         location.href = this.extedit.format(sid);
1186                                 }, this, section_id);
1187
1188                         L.dom.append(tdEl.lastElementChild,
1189                                 E('button', {
1190                                         'title': _('Edit'),
1191                                         'class': 'cbi-button cbi-button-edit',
1192                                         'click': evFn
1193                                 }, [ _('Edit') ])
1194                         );
1195                 }
1196
1197                 if (more_label) {
1198                         L.dom.append(tdEl.lastElementChild,
1199                                 E('button', {
1200                                         'title': more_label,
1201                                         'class': 'cbi-button cbi-button-edit',
1202                                         'click': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
1203                                 }, [ more_label ])
1204                         );
1205                 }
1206
1207                 if (this.addremove) {
1208                         var btn_title = this.titleFn('removebtntitle', section_id);
1209
1210                         L.dom.append(tdEl.lastElementChild,
1211                                 E('button', {
1212                                         'title': btn_title || _('Delete'),
1213                                         'class': 'cbi-button cbi-button-remove',
1214                                         'click': L.ui.createHandlerFn(this, function(sid, ev) {
1215                                                 this.map.data.remove(config_name, sid);
1216                                                 return this.map.save(null, true);
1217                                         }, section_id)
1218                                 }, [ btn_title || _('Delete') ])
1219                         );
1220                 }
1221
1222                 return tdEl;
1223         },
1224
1225         handleDragInit: function(ev) {
1226                 scope.dragState = { node: ev.target };
1227         },
1228
1229         handleDragStart: function(ev) {
1230                 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1231                         scope.dragState = null;
1232                         ev.preventDefault();
1233                         return false;
1234                 }
1235
1236                 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
1237                 ev.dataTransfer.setData('text', 'drag');
1238                 ev.target.style.opacity = 0.4;
1239         },
1240
1241         handleDragOver: function(ev) {
1242                 var n = scope.dragState.targetNode,
1243                     r = scope.dragState.rect,
1244                     t = r.top + r.height / 2;
1245
1246                 if (ev.clientY <= t) {
1247                         n.classList.remove('drag-over-below');
1248                         n.classList.add('drag-over-above');
1249                 }
1250                 else {
1251                         n.classList.remove('drag-over-above');
1252                         n.classList.add('drag-over-below');
1253                 }
1254
1255                 ev.dataTransfer.dropEffect = 'move';
1256                 ev.preventDefault();
1257                 return false;
1258         },
1259
1260         handleDragEnter: function(ev) {
1261                 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1262                 scope.dragState.targetNode = ev.currentTarget;
1263         },
1264
1265         handleDragLeave: function(ev) {
1266                 ev.currentTarget.classList.remove('drag-over-above');
1267                 ev.currentTarget.classList.remove('drag-over-below');
1268         },
1269
1270         handleDragEnd: function(ev) {
1271                 var n = ev.target;
1272
1273                 n.style.opacity = '';
1274                 n.classList.add('flash');
1275                 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1276                         .forEach(function(tr) {
1277                                 tr.classList.remove('drag-over-above');
1278                                 tr.classList.remove('drag-over-below');
1279                         });
1280         },
1281
1282         handleDrop: function(ev) {
1283                 var s = scope.dragState;
1284
1285                 if (s.node && s.targetNode) {
1286                         var config_name = this.uciconfig || this.map.config,
1287                             ref_node = s.targetNode,
1288                             after = false;
1289
1290                     if (ref_node.classList.contains('drag-over-below')) {
1291                         ref_node = ref_node.nextElementSibling;
1292                         after = true;
1293                     }
1294
1295                     var sid1 = s.node.getAttribute('data-sid'),
1296                         sid2 = s.targetNode.getAttribute('data-sid');
1297
1298                     s.node.parentNode.insertBefore(s.node, ref_node);
1299                     this.map.data.move(config_name, sid1, sid2, after);
1300                 }
1301
1302                 scope.dragState = null;
1303                 ev.target.style.opacity = '';
1304                 ev.stopPropagation();
1305                 ev.preventDefault();
1306                 return false;
1307         },
1308
1309         handleModalCancel: function(modalMap, ev) {
1310                 return Promise.resolve(L.ui.hideModal());
1311         },
1312
1313         handleModalSave: function(modalMap, ev) {
1314                 return modalMap.save()
1315                         .then(L.bind(this.map.load, this.map))
1316                         .then(L.bind(this.map.reset, this.map))
1317                         .then(L.ui.hideModal)
1318                         .catch(function() {});
1319         },
1320
1321         addModalOptions: function(modalSection, section_id, ev) {
1322
1323         },
1324
1325         renderMoreOptionsModal: function(section_id, ev) {
1326                 var parent = this.map,
1327                     title = parent.title,
1328                     name = null,
1329                     m = new CBIMap(this.map.config, null, null),
1330                     s = m.section(CBINamedSection, section_id, this.sectiontype);
1331
1332                 m.parent = parent;
1333
1334                 s.tabs = this.tabs;
1335                 s.tab_names = this.tab_names;
1336
1337                 if ((name = this.titleFn('modaltitle', section_id)) != null)
1338                         title = name;
1339                 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1340                         title = '%s - %s'.format(parent.title, name);
1341                 else if (!this.anonymous)
1342                         title = '%s - %s'.format(parent.title, section_id);
1343
1344                 for (var i = 0; i < this.children.length; i++) {
1345                         var o1 = this.children[i];
1346
1347                         if (o1.modalonly === false)
1348                                 continue;
1349
1350                         var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1351
1352                         for (var k in o1) {
1353                                 if (!o1.hasOwnProperty(k))
1354                                         continue;
1355
1356                                 switch (k) {
1357                                 case 'map':
1358                                 case 'section':
1359                                 case 'option':
1360                                 case 'title':
1361                                 case 'description':
1362                                         continue;
1363
1364                                 default:
1365                                         o2[k] = o1[k];
1366                                 }
1367                         }
1368                 }
1369
1370                 return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1371                         L.ui.showModal(title, [
1372                                 nodes,
1373                                 E('div', { 'class': 'right' }, [
1374                                         E('button', {
1375                                                 'class': 'btn',
1376                                                 'click': L.ui.createHandlerFn(this, 'handleModalCancel', m)
1377                                         }, [ _('Dismiss') ]), ' ',
1378                                         E('button', {
1379                                                 'class': 'cbi-button cbi-button-positive important',
1380                                                 'click': L.ui.createHandlerFn(this, 'handleModalSave', m)
1381                                         }, [ _('Save') ])
1382                                 ])
1383                         ], 'cbi-modal');
1384                 }, this)).catch(L.error);
1385         }
1386 });
1387
1388 var CBIGridSection = CBITableSection.extend({
1389         tab: function(name, title, description) {
1390                 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1391         },
1392
1393         handleAdd: function(ev) {
1394                 var config_name = this.uciconfig || this.map.config,
1395                     section_id = this.map.data.add(config_name, this.sectiontype);
1396
1397                 this.addedSection = section_id;
1398                 return this.renderMoreOptionsModal(section_id);
1399         },
1400
1401         handleModalSave: function(/* ... */) {
1402                 return this.super('handleModalSave', arguments)
1403                         .then(L.bind(function() { this.addedSection = null }, this));
1404         },
1405
1406         handleModalCancel: function(/* ... */) {
1407                 var config_name = this.uciconfig || this.map.config;
1408
1409                 if (this.addedSection != null) {
1410                         this.map.data.remove(config_name, this.addedSection);
1411                         this.addedSection = null;
1412                 }
1413
1414                 return this.super('handleModalCancel', arguments);
1415         },
1416
1417         renderUCISection: function(section_id) {
1418                 return this.renderOptions(null, section_id);
1419         },
1420
1421         renderChildren: function(tab_name, section_id, in_table) {
1422                 var tasks = [], index = 0;
1423
1424                 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1425                         if (opt.disable || opt.modalonly)
1426                                 continue;
1427
1428                         if (opt.editable)
1429                                 tasks.push(opt.render(index++, section_id, in_table));
1430                         else
1431                                 tasks.push(this.renderTextValue(section_id, opt));
1432                 }
1433
1434                 return Promise.all(tasks);
1435         },
1436
1437         renderTextValue: function(section_id, opt) {
1438                 var title = this.stripTags(opt.title).trim(),
1439                     descr = this.stripTags(opt.description).trim(),
1440                     value = opt.textvalue(section_id);
1441
1442                 return E('div', {
1443                         'class': 'td cbi-value-field',
1444                         'data-title': (title != '') ? title : null,
1445                         'data-description': (descr != '') ? descr : null,
1446                         'data-name': opt.option,
1447                         'data-type': opt.typename || opt.__name__
1448                 }, (value != null) ? value : E('em', _('none')));
1449         },
1450
1451         renderRowActions: function(section_id) {
1452                 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1453         },
1454
1455         parse: function() {
1456                 var section_ids = this.cfgsections(),
1457                     tasks = [];
1458
1459                 if (Array.isArray(this.children)) {
1460                         for (var i = 0; i < section_ids.length; i++) {
1461                                 for (var j = 0; j < this.children.length; j++) {
1462                                         if (!this.children[j].editable || this.children[j].modalonly)
1463                                                 continue;
1464
1465                                         tasks.push(this.children[j].parse(section_ids[i]));
1466                                 }
1467                         }
1468                 }
1469
1470                 return Promise.all(tasks);
1471         }
1472 });
1473
1474 var CBINamedSection = CBIAbstractSection.extend({
1475         __name__: 'CBI.NamedSection',
1476         __init__: function(map, section_id /*, ... */) {
1477                 this.super('__init__', this.varargs(arguments, 2, map));
1478
1479                 this.section = section_id;
1480         },
1481
1482         cfgsections: function() {
1483                 return [ this.section ];
1484         },
1485
1486         handleAdd: function(ev) {
1487                 var section_id = this.section,
1488                     config_name = this.uciconfig || this.map.config;
1489
1490                 this.map.data.add(config_name, this.sectiontype, section_id);
1491                 return this.map.save(null, true);
1492         },
1493
1494         handleRemove: function(ev) {
1495                 var section_id = this.section,
1496                     config_name = this.uciconfig || this.map.config;
1497
1498                 this.map.data.remove(config_name, section_id);
1499                 return this.map.save(null, true);
1500         },
1501
1502         renderContents: function(data) {
1503                 var ucidata = data[0], nodes = data[1],
1504                     section_id = this.section,
1505                     config_name = this.uciconfig || this.map.config,
1506                     sectionEl = E('div', {
1507                                 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1508                                 'class': 'cbi-section',
1509                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
1510                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
1511                         });
1512
1513                 if (typeof(this.title) === 'string' && this.title !== '')
1514                         sectionEl.appendChild(E('legend', {}, this.title));
1515
1516                 if (typeof(this.description) === 'string' && this.description !== '')
1517                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1518
1519                 if (ucidata) {
1520                         if (this.addremove) {
1521                                 sectionEl.appendChild(
1522                                         E('div', { 'class': 'cbi-section-remove right' },
1523                                                 E('button', {
1524                                                         'class': 'cbi-button',
1525                                                         'click': L.ui.createHandlerFn(this, 'handleRemove')
1526                                                 }, [ _('Delete') ])));
1527                         }
1528
1529                         sectionEl.appendChild(E('div', {
1530                                 'id': 'cbi-%s-%s'.format(config_name, section_id),
1531                                 'class': this.tabs
1532                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1533                                 'data-section-id': section_id
1534                         }, nodes));
1535                 }
1536                 else if (this.addremove) {
1537                         sectionEl.appendChild(
1538                                 E('button', {
1539                                         'class': 'cbi-button cbi-button-add',
1540                                         'click': L.ui.createHandlerFn(this, 'handleAdd')
1541                                 }, [ _('Add') ]));
1542                 }
1543
1544                 L.dom.bindClassInstance(sectionEl, this);
1545
1546                 return sectionEl;
1547         },
1548
1549         render: function() {
1550                 var config_name = this.uciconfig || this.map.config,
1551                     section_id = this.section;
1552
1553                 return Promise.all([
1554                         this.map.data.get(config_name, section_id),
1555                         this.renderUCISection(section_id)
1556                 ]).then(this.renderContents.bind(this));
1557         }
1558 });
1559
1560 var CBIValue = CBIAbstractValue.extend({
1561         __name__: 'CBI.Value',
1562
1563         value: function(key, val) {
1564                 this.keylist = this.keylist || [];
1565                 this.keylist.push(String(key));
1566
1567                 this.vallist = this.vallist || [];
1568                 this.vallist.push(String(val != null ? val : key));
1569         },
1570
1571         render: function(option_index, section_id, in_table) {
1572                 return Promise.resolve(this.cfgvalue(section_id))
1573                         .then(this.renderWidget.bind(this, section_id, option_index))
1574                         .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1575         },
1576
1577         renderFrame: function(section_id, in_table, option_index, nodes) {
1578                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1579                     depend_list = this.transformDepList(section_id),
1580                     optionEl;
1581
1582                 if (in_table) {
1583                         var title = this.stripTags(this.title).trim();
1584                         optionEl = E('div', {
1585                                 'class': 'td cbi-value-field',
1586                                 'data-title': (title != '') ? title : null,
1587                                 'data-description': this.stripTags(this.description).trim(),
1588                                 'data-name': this.option,
1589                                 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1590                         }, E('div', {
1591                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1592                                 'data-index': option_index,
1593                                 'data-depends': depend_list,
1594                                 'data-field': this.cbid(section_id)
1595                         }));
1596                 }
1597                 else {
1598                         optionEl = E('div', {
1599                                 'class': 'cbi-value',
1600                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1601                                 'data-index': option_index,
1602                                 'data-depends': depend_list,
1603                                 'data-field': this.cbid(section_id),
1604                                 'data-name': this.option,
1605                                 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1606                         });
1607
1608                         if (this.last_child)
1609                                 optionEl.classList.add('cbi-value-last');
1610
1611                         if (typeof(this.title) === 'string' && this.title !== '') {
1612                                 optionEl.appendChild(E('label', {
1613                                         'class': 'cbi-value-title',
1614                                         'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
1615                                 },
1616                                 this.titleref ? E('a', {
1617                                         'class': 'cbi-title-ref',
1618                                         'href': this.titleref,
1619                                         'title': this.titledesc || _('Go to relevant configuration page')
1620                                 }, this.title) : this.title));
1621
1622                                 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1623                         }
1624                 }
1625
1626                 if (nodes)
1627                         (optionEl.lastChild || optionEl).appendChild(nodes);
1628
1629                 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1630                         L.dom.append(optionEl.lastChild || optionEl,
1631                                 E('div', { 'class': 'cbi-value-description' }, this.description));
1632
1633                 if (depend_list && depend_list.length)
1634                         optionEl.classList.add('hidden');
1635
1636                 optionEl.addEventListener('widget-change',
1637                         L.bind(this.map.checkDepends, this.map));
1638
1639                 L.dom.bindClassInstance(optionEl, this);
1640
1641                 return optionEl;
1642         },
1643
1644         renderWidget: function(section_id, option_index, cfgvalue) {
1645                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1646                     choices = this.transformChoices(),
1647                     widget;
1648
1649                 if (choices) {
1650                         var placeholder = (this.optional || this.rmempty)
1651                                 ? E('em', _('unspecified')) : _('-- Please choose --');
1652
1653                         widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1654                                 id: this.cbid(section_id),
1655                                 sort: this.keylist,
1656                                 optional: this.optional || this.rmempty,
1657                                 datatype: this.datatype,
1658                                 select_placeholder: this.placeholder || placeholder,
1659                                 validate: L.bind(this.validate, this, section_id)
1660                         });
1661                 }
1662                 else {
1663                         widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1664                                 id: this.cbid(section_id),
1665                                 password: this.password,
1666                                 optional: this.optional || this.rmempty,
1667                                 datatype: this.datatype,
1668                                 placeholder: this.placeholder,
1669                                 validate: L.bind(this.validate, this, section_id)
1670                         });
1671                 }
1672
1673                 return widget.render();
1674         }
1675 });
1676
1677 var CBIDynamicList = CBIValue.extend({
1678         __name__: 'CBI.DynamicList',
1679
1680         renderWidget: function(section_id, option_index, cfgvalue) {
1681                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1682                     choices = this.transformChoices(),
1683                     items = L.toArray(value);
1684
1685                 var widget = new ui.DynamicList(items, choices, {
1686                         id: this.cbid(section_id),
1687                         sort: this.keylist,
1688                         optional: this.optional || this.rmempty,
1689                         datatype: this.datatype,
1690                         placeholder: this.placeholder,
1691                         validate: L.bind(this.validate, this, section_id)
1692                 });
1693
1694                 return widget.render();
1695         },
1696 });
1697
1698 var CBIListValue = CBIValue.extend({
1699         __name__: 'CBI.ListValue',
1700
1701         __init__: function() {
1702                 this.super('__init__', arguments);
1703                 this.widget = 'select';
1704                 this.deplist = [];
1705         },
1706
1707         renderWidget: function(section_id, option_index, cfgvalue) {
1708                 var choices = this.transformChoices();
1709                 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1710                         id: this.cbid(section_id),
1711                         size: this.size,
1712                         sort: this.keylist,
1713                         optional: this.optional,
1714                         placeholder: this.placeholder,
1715                         validate: L.bind(this.validate, this, section_id)
1716                 });
1717
1718                 return widget.render();
1719         },
1720 });
1721
1722 var CBIFlagValue = CBIValue.extend({
1723         __name__: 'CBI.FlagValue',
1724
1725         __init__: function() {
1726                 this.super('__init__', arguments);
1727
1728                 this.enabled = '1';
1729                 this.disabled = '0';
1730                 this.default = this.disabled;
1731         },
1732
1733         renderWidget: function(section_id, option_index, cfgvalue) {
1734                 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1735                         id: this.cbid(section_id),
1736                         value_enabled: this.enabled,
1737                         value_disabled: this.disabled,
1738                         validate: L.bind(this.validate, this, section_id)
1739                 });
1740
1741                 return widget.render();
1742         },
1743
1744         formvalue: function(section_id) {
1745                 var node = this.map.findElement('id', this.cbid(section_id)),
1746                     checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
1747
1748                 return checked ? this.enabled : this.disabled;
1749         },
1750
1751         textvalue: function(section_id) {
1752                 var cval = this.cfgvalue(section_id);
1753
1754                 if (cval == null)
1755                         cval = this.default;
1756
1757                 return (cval == this.enabled) ? _('Yes') : _('No');
1758         },
1759
1760         parse: function(section_id) {
1761                 if (this.isActive(section_id)) {
1762                         var fval = this.formvalue(section_id);
1763
1764                         if (!this.isValid(section_id))
1765                                 return Promise.reject();
1766
1767                         if (fval == this.default && (this.optional || this.rmempty))
1768                                 return Promise.resolve(this.remove(section_id));
1769                         else
1770                                 return Promise.resolve(this.write(section_id, fval));
1771                 }
1772                 else {
1773                         return Promise.resolve(this.remove(section_id));
1774                 }
1775         },
1776 });
1777
1778 var CBIMultiValue = CBIDynamicList.extend({
1779         __name__: 'CBI.MultiValue',
1780
1781         __init__: function() {
1782                 this.super('__init__', arguments);
1783                 this.placeholder = _('-- Please choose --');
1784         },
1785
1786         renderWidget: function(section_id, option_index, cfgvalue) {
1787                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1788                     choices = this.transformChoices();
1789
1790                 var widget = new ui.Dropdown(L.toArray(value), choices, {
1791                         id: this.cbid(section_id),
1792                         sort: this.keylist,
1793                         multiple: true,
1794                         optional: this.optional || this.rmempty,
1795                         select_placeholder: this.placeholder,
1796                         display_items: this.display_size || this.size || 3,
1797                         dropdown_items: this.dropdown_size || this.size || -1,
1798                         validate: L.bind(this.validate, this, section_id)
1799                 });
1800
1801                 return widget.render();
1802         },
1803 });
1804
1805 var CBITextValue = CBIValue.extend({
1806         __name__: 'CBI.TextValue',
1807
1808         value: null,
1809
1810         renderWidget: function(section_id, option_index, cfgvalue) {
1811                 var value = (cfgvalue != null) ? cfgvalue : this.default;
1812
1813                 var widget = new ui.Textarea(value, {
1814                         id: this.cbid(section_id),
1815                         optional: this.optional || this.rmempty,
1816                         placeholder: this.placeholder,
1817                         monospace: this.monospace,
1818                         cols: this.cols,
1819                         rows: this.rows,
1820                         wrap: this.wrap,
1821                         validate: L.bind(this.validate, this, section_id)
1822                 });
1823
1824                 return widget.render();
1825         }
1826 });
1827
1828 var CBIDummyValue = CBIValue.extend({
1829         __name__: 'CBI.DummyValue',
1830
1831         renderWidget: function(section_id, option_index, cfgvalue) {
1832                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1833                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1834                     outputEl = E('div');
1835
1836                 if (this.href)
1837                         outputEl.appendChild(E('a', { 'href': this.href }));
1838
1839                 L.dom.append(outputEl.lastChild || outputEl,
1840                         this.rawhtml ? value : [ value ]);
1841
1842                 return E([
1843                         outputEl,
1844                         hiddenEl.render()
1845                 ]);
1846         },
1847
1848         remove: function() {},
1849         write: function() {}
1850 });
1851
1852 var CBIButtonValue = CBIValue.extend({
1853         __name__: 'CBI.ButtonValue',
1854
1855         renderWidget: function(section_id, option_index, cfgvalue) {
1856                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1857                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1858                     outputEl = E('div'),
1859                     btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1860
1861                 if (value !== false)
1862                         L.dom.content(outputEl, [
1863                                 E('button', {
1864                                         'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1865                                         'click': L.ui.createHandlerFn(this, this.onclick || function(ev) {
1866                                                 ev.currentTarget.parentNode.nextElementSibling.value = value;
1867                                                 return this.map.save();
1868                                         })
1869                                 }, [ btn_title ])
1870                         ]);
1871                 else
1872                         L.dom.content(outputEl, ' - ');
1873
1874                 return E([
1875                         outputEl,
1876                         hiddenEl.render()
1877                 ]);
1878         }
1879 });
1880
1881 var CBIHiddenValue = CBIValue.extend({
1882         __name__: 'CBI.HiddenValue',
1883
1884         renderWidget: function(section_id, option_index, cfgvalue) {
1885                 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1886                         id: this.cbid(section_id)
1887                 });
1888
1889                 return widget.render();
1890         }
1891 });
1892
1893 var CBIFileUpload = CBIValue.extend({
1894         __name__: 'CBI.FileSelect',
1895
1896         __init__: function(/* ... */) {
1897                 this.super('__init__', arguments);
1898
1899                 this.show_hidden = false;
1900                 this.enable_upload = true;
1901                 this.enable_remove = true;
1902                 this.root_directory = '/etc/luci-uploads';
1903         },
1904
1905         renderWidget: function(section_id, option_index, cfgvalue) {
1906                 var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
1907                         id: this.cbid(section_id),
1908                         name: this.cbid(section_id),
1909                         show_hidden: this.show_hidden,
1910                         enable_upload: this.enable_upload,
1911                         enable_remove: this.enable_remove,
1912                         root_directory: this.root_directory
1913                 });
1914
1915                 return browserEl.render();
1916         }
1917 });
1918
1919 var CBISectionValue = CBIValue.extend({
1920         __name__: 'CBI.ContainerValue',
1921         __init__: function(map, section, option, cbiClass /*, ... */) {
1922                 this.super('__init__', [map, section, option]);
1923
1924                 if (!CBIAbstractSection.isSubclass(cbiClass))
1925                         throw 'Sub section must be a descendent of CBIAbstractSection';
1926
1927                 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1928         },
1929
1930         load: function(section_id) {
1931                 return this.subsection.load();
1932         },
1933
1934         parse: function(section_id) {
1935                 return this.subsection.parse();
1936         },
1937
1938         renderWidget: function(section_id, option_index, cfgvalue) {
1939                 return this.subsection.render();
1940         },
1941
1942         checkDepends: function(section_id) {
1943                 this.subsection.checkDepends();
1944                 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1945         },
1946
1947         write: function() {},
1948         remove: function() {},
1949         cfgvalue: function() { return null },
1950         formvalue: function() { return null }
1951 });
1952
1953 return L.Class.extend({
1954         Map: CBIMap,
1955         JSONMap: CBIJSONMap,
1956         AbstractSection: CBIAbstractSection,
1957         AbstractValue: CBIAbstractValue,
1958
1959         TypedSection: CBITypedSection,
1960         TableSection: CBITableSection,
1961         GridSection: CBIGridSection,
1962         NamedSection: CBINamedSection,
1963
1964         Value: CBIValue,
1965         DynamicList: CBIDynamicList,
1966         ListValue: CBIListValue,
1967         Flag: CBIFlagValue,
1968         MultiValue: CBIMultiValue,
1969         TextValue: CBITextValue,
1970         DummyValue: CBIDummyValue,
1971         Button: CBIButtonValue,
1972         HiddenValue: CBIHiddenValue,
1973         FileUpload: CBIFileUpload,
1974         SectionValue: CBISectionValue
1975 });