luci-base: form.js / ui.js: tie form labels to widgets
[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 = depends[i]['!reverse'],
407                             contains = depends[i]['!contains'];
408
409                         for (var dep in depends[i]) {
410                                 if (dep == '!reverse' || dep == '!contains') {
411                                         continue;
412                                 }
413                                 else if (dep == '!default') {
414                                         def = true;
415                                         istat = false;
416                                 }
417                                 else {
418                                         var res = this.lookupOption(dep, section_id, config_name),
419                                             val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
420
421                                         var equal = contains
422                                                 ? isContained(val, depends[i][dep])
423                                                 : isEqual(val, depends[i][dep]);
424
425                                         istat = (istat && equal);
426                                 }
427                         }
428
429                         if (istat ^ reverse)
430                                 return true;
431                 }
432
433                 return def;
434         }
435 });
436
437 var CBIJSONMap = CBIMap.extend({
438         __init__: function(data /*, ... */) {
439                 this.super('__init__', this.varargs(arguments, 1, 'json'));
440
441                 this.config = 'json';
442                 this.parsechain = [ 'json' ];
443                 this.data = new CBIJSONConfig(data);
444         }
445 });
446
447 var CBIAbstractSection = CBINode.extend({
448         __init__: function(map, sectionType /*, ... */) {
449                 this.super('__init__', this.varargs(arguments, 2));
450
451                 this.sectiontype = sectionType;
452                 this.map = map;
453                 this.config = map.config;
454
455                 this.optional = true;
456                 this.addremove = false;
457                 this.dynamic = false;
458         },
459
460         cfgsections: function() {
461                 L.error('InternalError', 'Not implemented');
462         },
463
464         filter: function(section_id) {
465                 return true;
466         },
467
468         load: function() {
469                 var section_ids = this.cfgsections(),
470                     tasks = [];
471
472                 if (Array.isArray(this.children))
473                         for (var i = 0; i < section_ids.length; i++)
474                                 tasks.push(this.loadChildren(section_ids[i])
475                                         .then(Function.prototype.bind.call(function(section_id, set_values) {
476                                                 for (var i = 0; i < set_values.length; i++)
477                                                         this.children[i].cfgvalue(section_id, set_values[i]);
478                                         }, this, section_ids[i])));
479
480                 return Promise.all(tasks);
481         },
482
483         parse: function() {
484                 var section_ids = this.cfgsections(),
485                     tasks = [];
486
487                 if (Array.isArray(this.children))
488                         for (var i = 0; i < section_ids.length; i++)
489                                 for (var j = 0; j < this.children.length; j++)
490                                         tasks.push(this.children[j].parse(section_ids[i]));
491
492                 return Promise.all(tasks);
493         },
494
495         tab: function(name, title, description) {
496                 if (this.tabs && this.tabs[name])
497                         throw 'Tab already declared';
498
499                 var entry = {
500                         name: name,
501                         title: title,
502                         description: description,
503                         children: []
504                 };
505
506                 this.tabs = this.tabs || [];
507                 this.tabs.push(entry);
508                 this.tabs[name] = entry;
509
510                 this.tab_names = this.tab_names || [];
511                 this.tab_names.push(name);
512         },
513
514         option: function(cbiClass /*, ... */) {
515                 if (!CBIAbstractValue.isSubclass(cbiClass))
516                         throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
517
518                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
519                 this.append(obj);
520                 return obj;
521         },
522
523         taboption: function(tabName /*, ... */) {
524                 if (!this.tabs || !this.tabs[tabName])
525                         throw L.error('ReferenceError', 'Associated tab not declared');
526
527                 var obj = this.option.apply(this, this.varargs(arguments, 1));
528                 obj.tab = tabName;
529                 this.tabs[tabName].children.push(obj);
530                 return obj;
531         },
532
533         renderUCISection: function(section_id) {
534                 var renderTasks = [];
535
536                 if (!this.tabs)
537                         return this.renderOptions(null, section_id);
538
539                 for (var i = 0; i < this.tab_names.length; i++)
540                         renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
541
542                 return Promise.all(renderTasks)
543                         .then(this.renderTabContainers.bind(this, section_id));
544         },
545
546         renderTabContainers: function(section_id, nodes) {
547                 var config_name = this.uciconfig || this.map.config,
548                     containerEls = E([]);
549
550                 for (var i = 0; i < nodes.length; i++) {
551                         var tab_name = this.tab_names[i],
552                             tab_data = this.tabs[tab_name],
553                             containerEl = E('div', {
554                                 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
555                                 'data-tab': tab_name,
556                                 'data-tab-title': tab_data.title,
557                                 'data-tab-active': tab_name === this.selected_tab
558                             });
559
560                         if (tab_data.description != null && tab_data.description != '')
561                                 containerEl.appendChild(
562                                         E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
563
564                         containerEl.appendChild(nodes[i]);
565                         containerEls.appendChild(containerEl);
566                 }
567
568                 return containerEls;
569         },
570
571         renderOptions: function(tab_name, section_id) {
572                 var in_table = (this instanceof CBITableSection);
573                 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
574                         var optionEls = E([]);
575                         for (var i = 0; i < nodes.length; i++)
576                                 optionEls.appendChild(nodes[i]);
577                         return optionEls;
578                 });
579         },
580
581         checkDepends: function(ev, n) {
582                 var changed = false,
583                     sids = this.cfgsections();
584
585                 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
586                         for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
587                                 var isActive = o.isActive(sid),
588                                     isSatisified = o.checkDepends(sid);
589
590                                 if (isActive != isSatisified) {
591                                         o.setActive(sid, !isActive);
592                                         isActive = !isActive;
593                                         changed = true;
594                                 }
595
596                                 if (!n && isActive)
597                                         o.triggerValidation(sid);
598                         }
599                 }
600
601                 return changed;
602         }
603 });
604
605
606 var isEqual = function(x, y) {
607         if (x != null && y != null && typeof(x) != typeof(y))
608                 return false;
609
610         if ((x == null && y != null) || (x != null && y == null))
611                 return false;
612
613         if (Array.isArray(x)) {
614                 if (x.length != y.length)
615                         return false;
616
617                 for (var i = 0; i < x.length; i++)
618                         if (!isEqual(x[i], y[i]))
619                                 return false;
620         }
621         else if (typeof(x) == 'object') {
622                 for (var k in x) {
623                         if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
624                                 return false;
625
626                         if (!isEqual(x[k], y[k]))
627                                 return false;
628                 }
629
630                 for (var k in y)
631                         if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
632                                 return false;
633         }
634         else if (x != y) {
635                 return false;
636         }
637
638         return true;
639 };
640
641 var isContained = function(x, y) {
642         if (Array.isArray(x)) {
643                 for (var i = 0; i < x.length; i++)
644                         if (x[i] == y)
645                                 return true;
646         }
647         else if (L.isObject(x)) {
648                 if (x.hasOwnProperty(y) && x[y] != null)
649                         return true;
650         }
651         else if (typeof(x) == 'string') {
652                 return (x.indexOf(y) > -1);
653         }
654
655         return false;
656 };
657
658 var CBIAbstractValue = CBINode.extend({
659         __init__: function(map, section, option /*, ... */) {
660                 this.super('__init__', this.varargs(arguments, 3));
661
662                 this.section = section;
663                 this.option = option;
664                 this.map = map;
665                 this.config = map.config;
666
667                 this.deps = [];
668                 this.initial = {};
669                 this.rmempty = true;
670                 this.default = null;
671                 this.size = null;
672                 this.optional = false;
673         },
674
675         depends: function(field, value) {
676                 var deps;
677
678                 if (typeof(field) === 'string')
679                         deps = {}, deps[field] = value;
680                 else
681                         deps = field;
682
683                 this.deps.push(deps);
684         },
685
686         transformDepList: function(section_id, deplist) {
687                 var list = deplist || this.deps,
688                     deps = [];
689
690                 if (Array.isArray(list)) {
691                         for (var i = 0; i < list.length; i++) {
692                                 var dep = {};
693
694                                 for (var k in list[i]) {
695                                         if (list[i].hasOwnProperty(k)) {
696                                                 if (k.charAt(0) === '!')
697                                                         dep[k] = list[i][k];
698                                                 else if (k.indexOf('.') !== -1)
699                                                         dep['cbid.%s'.format(k)] = list[i][k];
700                                                 else
701                                                         dep['cbid.%s.%s.%s'.format(
702                                                                 this.uciconfig || this.section.uciconfig || this.map.config,
703                                                                 this.ucisection || section_id,
704                                                                 k
705                                                         )] = list[i][k];
706                                         }
707                                 }
708
709                                 for (var k in dep) {
710                                         if (dep.hasOwnProperty(k)) {
711                                                 deps.push(dep);
712                                                 break;
713                                         }
714                                 }
715                         }
716                 }
717
718                 return deps;
719         },
720
721         transformChoices: function() {
722                 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
723                         return null;
724
725                 var choices = {};
726
727                 for (var i = 0; i < this.keylist.length; i++)
728                         choices[this.keylist[i]] = this.vallist[i];
729
730                 return choices;
731         },
732
733         checkDepends: function(section_id) {
734                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
735                     active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
736
737                 if (active)
738                         this.updateDefaultValue(section_id);
739
740                 return active;
741         },
742
743         updateDefaultValue: function(section_id) {
744                 if (!L.isObject(this.defaults))
745                         return;
746
747                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
748                     cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
749                     default_defval = null, satisified_defval = null;
750
751                 for (var value in this.defaults) {
752                         if (!this.defaults[value] || this.defaults[value].length == 0) {
753                                 default_defval = value;
754                                 continue;
755                         }
756                         else if (this.map.isDependencySatisfied(this.defaults[value], config_name, section_id)) {
757                                 satisified_defval = value;
758                                 break;
759                         }
760                 }
761
762                 if (satisified_defval == null)
763                         satisified_defval = default_defval;
764
765                 var node = this.map.findElement('id', this.cbid(section_id));
766                 if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
767                         L.dom.callClassMethod(node, 'setValue', satisified_defval);
768
769                 this.default = satisified_defval;
770         },
771
772         cbid: function(section_id) {
773                 if (section_id == null)
774                         L.error('TypeError', 'Section ID required');
775
776                 return 'cbid.%s.%s.%s'.format(
777                         this.uciconfig || this.section.uciconfig || this.map.config,
778                         section_id, this.option);
779         },
780
781         load: function(section_id) {
782                 if (section_id == null)
783                         L.error('TypeError', 'Section ID required');
784
785                 return this.map.data.get(
786                         this.uciconfig || this.section.uciconfig || this.map.config,
787                         this.ucisection || section_id,
788                         this.ucioption || this.option);
789         },
790
791         getUIElement: function(section_id) {
792                 var node = this.map.findElement('id', this.cbid(section_id)),
793                     inst = node ? L.dom.findClassInstance(node) : null;
794                 return (inst instanceof ui.AbstractElement) ? inst : null;
795         },
796
797         cfgvalue: function(section_id, set_value) {
798                 if (section_id == null)
799                         L.error('TypeError', 'Section ID required');
800
801                 if (arguments.length == 2) {
802                         this.data = this.data || {};
803                         this.data[section_id] = set_value;
804                 }
805
806                 return this.data ? this.data[section_id] : null;
807         },
808
809         formvalue: function(section_id) {
810                 var elem = this.getUIElement(section_id);
811                 return elem ? elem.getValue() : null;
812         },
813
814         textvalue: function(section_id) {
815                 var cval = this.cfgvalue(section_id);
816
817                 if (cval == null)
818                         cval = this.default;
819
820                 return (cval != null) ? '%h'.format(cval) : null;
821         },
822
823         validate: function(section_id, value) {
824                 return true;
825         },
826
827         isValid: function(section_id) {
828                 var elem = this.getUIElement(section_id);
829                 return elem ? elem.isValid() : true;
830         },
831
832         isActive: function(section_id) {
833                 var field = this.map.findElement('data-field', this.cbid(section_id));
834                 return (field != null && !field.classList.contains('hidden'));
835         },
836
837         setActive: function(section_id, active) {
838                 var field = this.map.findElement('data-field', this.cbid(section_id));
839
840                 if (field && field.classList.contains('hidden') == active) {
841                         field.classList[active ? 'remove' : 'add']('hidden');
842                         return true;
843                 }
844
845                 return false;
846         },
847
848         triggerValidation: function(section_id) {
849                 var elem = this.getUIElement(section_id);
850                 return elem ? elem.triggerValidation() : true;
851         },
852
853         parse: function(section_id) {
854                 var active = this.isActive(section_id),
855                     cval = this.cfgvalue(section_id),
856                     fval = active ? this.formvalue(section_id) : null;
857
858                 if (active && !this.isValid(section_id))
859                         return Promise.reject();
860
861                 if (fval != '' && fval != null) {
862                         if (this.forcewrite || !isEqual(cval, fval))
863                                 return Promise.resolve(this.write(section_id, fval));
864                 }
865                 else {
866                         if (!active || this.rmempty || this.optional) {
867                                 return Promise.resolve(this.remove(section_id));
868                         }
869                         else if (!isEqual(cval, fval)) {
870                                 console.log('This should have been catched by isValid()');
871                                 return Promise.reject();
872                         }
873                 }
874
875                 return Promise.resolve();
876         },
877
878         write: function(section_id, formvalue) {
879                 return this.map.data.set(
880                         this.uciconfig || this.section.uciconfig || this.map.config,
881                         this.ucisection || section_id,
882                         this.ucioption || this.option,
883                         formvalue);
884         },
885
886         remove: function(section_id) {
887                 return this.map.data.unset(
888                         this.uciconfig || this.section.uciconfig || this.map.config,
889                         this.ucisection || section_id,
890                         this.ucioption || this.option);
891         }
892 });
893
894 var CBITypedSection = CBIAbstractSection.extend({
895         __name__: 'CBI.TypedSection',
896
897         cfgsections: function() {
898                 return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
899                         .map(function(s) { return s['.name'] })
900                         .filter(L.bind(this.filter, this));
901         },
902
903         handleAdd: function(ev, name) {
904                 var config_name = this.uciconfig || this.map.config;
905
906                 this.map.data.add(config_name, this.sectiontype, name);
907                 return this.map.save(null, true);
908         },
909
910         handleRemove: function(section_id, ev) {
911                 var config_name = this.uciconfig || this.map.config;
912
913                 this.map.data.remove(config_name, section_id);
914                 return this.map.save(null, true);
915         },
916
917         renderSectionAdd: function(extra_class) {
918                 if (!this.addremove)
919                         return E([]);
920
921                 var createEl = E('div', { 'class': 'cbi-section-create' }),
922                     config_name = this.uciconfig || this.map.config,
923                     btn_title = this.titleFn('addbtntitle');
924
925                 if (extra_class != null)
926                         createEl.classList.add(extra_class);
927
928                 if (this.anonymous) {
929                         createEl.appendChild(E('button', {
930                                 'class': 'cbi-button cbi-button-add',
931                                 'title': btn_title || _('Add'),
932                                 'click': L.ui.createHandlerFn(this, 'handleAdd')
933                         }, [ btn_title || _('Add') ]));
934                 }
935                 else {
936                         var nameEl = E('input', {
937                                 'type': 'text',
938                                 'class': 'cbi-section-create-name'
939                         });
940
941                         L.dom.append(createEl, [
942                                 E('div', {}, nameEl),
943                                 E('input', {
944                                         'class': 'cbi-button cbi-button-add',
945                                         'type': 'submit',
946                                         'value': btn_title || _('Add'),
947                                         'title': btn_title || _('Add'),
948                                         'click': L.ui.createHandlerFn(this, function(ev) {
949                                                 if (nameEl.classList.contains('cbi-input-invalid'))
950                                                         return;
951
952                                                 return this.handleAdd(ev, nameEl.value);
953                                         })
954                                 })
955                         ]);
956
957                         ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
958                 }
959
960                 return createEl;
961         },
962
963         renderSectionPlaceholder: function() {
964                 return E([
965                         E('em', _('This section contains no values yet')),
966                         E('br'), E('br')
967                 ]);
968         },
969
970         renderContents: function(cfgsections, nodes) {
971                 var section_id = null,
972                     config_name = this.uciconfig || this.map.config,
973                     sectionEl = E('div', {
974                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
975                                 'class': 'cbi-section',
976                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
977                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
978                         });
979
980                 if (this.title != null && this.title != '')
981                         sectionEl.appendChild(E('legend', {}, this.title));
982
983                 if (this.description != null && this.description != '')
984                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
985
986                 for (var i = 0; i < nodes.length; i++) {
987                         if (this.addremove) {
988                                 sectionEl.appendChild(
989                                         E('div', { 'class': 'cbi-section-remove right' },
990                                                 E('button', {
991                                                         'class': 'cbi-button',
992                                                         'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
993                                                         'data-section-id': cfgsections[i],
994                                                         'click': L.ui.createHandlerFn(this, 'handleRemove', cfgsections[i])
995                                                 }, [ _('Delete') ])));
996                         }
997
998                         if (!this.anonymous)
999                                 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
1000
1001                         sectionEl.appendChild(E('div', {
1002                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
1003                                 'class': this.tabs
1004                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1005                                 'data-section-id': cfgsections[i]
1006                         }, nodes[i]));
1007                 }
1008
1009                 if (nodes.length == 0)
1010                         sectionEl.appendChild(this.renderSectionPlaceholder());
1011
1012                 sectionEl.appendChild(this.renderSectionAdd());
1013
1014                 L.dom.bindClassInstance(sectionEl, this);
1015
1016                 return sectionEl;
1017         },
1018
1019         render: function() {
1020                 var cfgsections = this.cfgsections(),
1021                     renderTasks = [];
1022
1023                 for (var i = 0; i < cfgsections.length; i++)
1024                         renderTasks.push(this.renderUCISection(cfgsections[i]));
1025
1026                 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
1027         }
1028 });
1029
1030 var CBITableSection = CBITypedSection.extend({
1031         __name__: 'CBI.TableSection',
1032
1033         tab: function() {
1034                 throw 'Tabs are not supported by TableSection';
1035         },
1036
1037         renderContents: function(cfgsections, nodes) {
1038                 var section_id = null,
1039                     config_name = this.uciconfig || this.map.config,
1040                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1041                     has_more = max_cols < this.children.length,
1042                     sectionEl = E('div', {
1043                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
1044                                 'class': 'cbi-section cbi-tblsection',
1045                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
1046                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
1047                         }),
1048                         tableEl = E('div', {
1049                                 'class': 'table cbi-section-table'
1050                         });
1051
1052                 if (this.title != null && this.title != '')
1053                         sectionEl.appendChild(E('h3', {}, this.title));
1054
1055                 if (this.description != null && this.description != '')
1056                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1057
1058                 tableEl.appendChild(this.renderHeaderRows(max_cols));
1059
1060                 for (var i = 0; i < nodes.length; i++) {
1061                         var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
1062
1063                         if (sectionname == null)
1064                                 sectionname = cfgsections[i];
1065
1066                         var trEl = E('div', {
1067                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
1068                                 'class': 'tr cbi-section-table-row',
1069                                 'data-sid': cfgsections[i],
1070                                 'draggable': this.sortable ? true : null,
1071                                 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
1072                                 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
1073                                 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
1074                                 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
1075                                 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
1076                                 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
1077                                 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
1078                                 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
1079                                 'data-section-id': cfgsections[i]
1080                         });
1081
1082                         if (this.extedit || this.rowcolors)
1083                                 trEl.classList.add(!(tableEl.childNodes.length % 2)
1084                                         ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
1085
1086                         for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
1087                                 trEl.appendChild(nodes[i].firstChild);
1088
1089                         trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
1090                         tableEl.appendChild(trEl);
1091                 }
1092
1093                 if (nodes.length == 0)
1094                         tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
1095                                 E('div', { 'class': 'td' },
1096                                         E('em', {}, _('This section contains no values yet')))));
1097
1098                 sectionEl.appendChild(tableEl);
1099
1100                 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
1101
1102                 L.dom.bindClassInstance(sectionEl, this);
1103
1104                 return sectionEl;
1105         },
1106
1107         renderHeaderRows: function(max_cols, has_action) {
1108                 var has_titles = false,
1109                     has_descriptions = false,
1110                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
1111                     has_more = max_cols < this.children.length,
1112                     anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
1113                     trEls = E([]);
1114
1115                 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1116                         if (opt.modalonly)
1117                                 continue;
1118
1119                         has_titles = has_titles || !!opt.title;
1120                         has_descriptions = has_descriptions || !!opt.description;
1121                 }
1122
1123                 if (has_titles) {
1124                         var trEl = E('div', {
1125                                 'class': 'tr cbi-section-table-titles ' + anon_class,
1126                                 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
1127                         });
1128
1129                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1130                                 if (opt.modalonly)
1131                                         continue;
1132
1133                                 trEl.appendChild(E('div', {
1134                                         'class': 'th cbi-section-table-cell',
1135                                         'data-widget': opt.__name__
1136                                 }));
1137
1138                                 if (opt.width != null)
1139                                         trEl.lastElementChild.style.width =
1140                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1141
1142                                 if (opt.titleref)
1143                                         trEl.lastElementChild.appendChild(E('a', {
1144                                                 'href': opt.titleref,
1145                                                 'class': 'cbi-title-ref',
1146                                                 'title': this.titledesc || _('Go to relevant configuration page')
1147                                         }, opt.title));
1148                                 else
1149                                         L.dom.content(trEl.lastElementChild, opt.title);
1150                         }
1151
1152                         if (this.sortable || this.extedit || this.addremove || has_more || has_action)
1153                                 trEl.appendChild(E('div', {
1154                                         'class': 'th cbi-section-table-cell cbi-section-actions'
1155                                 }));
1156
1157                         trEls.appendChild(trEl);
1158                 }
1159
1160                 if (has_descriptions) {
1161                         var trEl = E('div', {
1162                                 'class': 'tr cbi-section-table-descr ' + anon_class
1163                         });
1164
1165                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
1166                                 if (opt.modalonly)
1167                                         continue;
1168
1169                                 trEl.appendChild(E('div', {
1170                                         'class': 'th cbi-section-table-cell',
1171                                         'data-widget': opt.__name__
1172                                 }, opt.description));
1173
1174                                 if (opt.width != null)
1175                                         trEl.lastElementChild.style.width =
1176                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
1177                         }
1178
1179                         if (this.sortable || this.extedit || this.addremove || has_more)
1180                                 trEl.appendChild(E('div', {
1181                                         'class': 'th cbi-section-table-cell cbi-section-actions'
1182                                 }));
1183
1184                         trEls.appendChild(trEl);
1185                 }
1186
1187                 return trEls;
1188         },
1189
1190         renderRowActions: function(section_id, more_label) {
1191                 var config_name = this.uciconfig || this.map.config;
1192
1193                 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
1194                         return E([]);
1195
1196                 var tdEl = E('div', {
1197                         'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
1198                 }, E('div'));
1199
1200                 if (this.sortable) {
1201                         L.dom.append(tdEl.lastElementChild, [
1202                                 E('div', {
1203                                         'title': _('Drag to reorder'),
1204                                         'class': 'btn cbi-button drag-handle center',
1205                                         'style': 'cursor:move'
1206                                 }, '☰')
1207                         ]);
1208                 }
1209
1210                 if (this.extedit) {
1211                         var evFn = null;
1212
1213                         if (typeof(this.extedit) == 'function')
1214                                 evFn = L.bind(this.extedit, this);
1215                         else if (typeof(this.extedit) == 'string')
1216                                 evFn = L.bind(function(sid, ev) {
1217                                         location.href = this.extedit.format(sid);
1218                                 }, this, section_id);
1219
1220                         L.dom.append(tdEl.lastElementChild,
1221                                 E('button', {
1222                                         'title': _('Edit'),
1223                                         'class': 'cbi-button cbi-button-edit',
1224                                         'click': evFn
1225                                 }, [ _('Edit') ])
1226                         );
1227                 }
1228
1229                 if (more_label) {
1230                         L.dom.append(tdEl.lastElementChild,
1231                                 E('button', {
1232                                         'title': more_label,
1233                                         'class': 'cbi-button cbi-button-edit',
1234                                         'click': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
1235                                 }, [ more_label ])
1236                         );
1237                 }
1238
1239                 if (this.addremove) {
1240                         var btn_title = this.titleFn('removebtntitle', section_id);
1241
1242                         L.dom.append(tdEl.lastElementChild,
1243                                 E('button', {
1244                                         'title': btn_title || _('Delete'),
1245                                         'class': 'cbi-button cbi-button-remove',
1246                                         'click': L.ui.createHandlerFn(this, 'handleRemove', section_id)
1247                                 }, [ btn_title || _('Delete') ])
1248                         );
1249                 }
1250
1251                 return tdEl;
1252         },
1253
1254         handleDragInit: function(ev) {
1255                 scope.dragState = { node: ev.target };
1256         },
1257
1258         handleDragStart: function(ev) {
1259                 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1260                         scope.dragState = null;
1261                         ev.preventDefault();
1262                         return false;
1263                 }
1264
1265                 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
1266                 ev.dataTransfer.setData('text', 'drag');
1267                 ev.target.style.opacity = 0.4;
1268         },
1269
1270         handleDragOver: function(ev) {
1271                 var n = scope.dragState.targetNode,
1272                     r = scope.dragState.rect,
1273                     t = r.top + r.height / 2;
1274
1275                 if (ev.clientY <= t) {
1276                         n.classList.remove('drag-over-below');
1277                         n.classList.add('drag-over-above');
1278                 }
1279                 else {
1280                         n.classList.remove('drag-over-above');
1281                         n.classList.add('drag-over-below');
1282                 }
1283
1284                 ev.dataTransfer.dropEffect = 'move';
1285                 ev.preventDefault();
1286                 return false;
1287         },
1288
1289         handleDragEnter: function(ev) {
1290                 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1291                 scope.dragState.targetNode = ev.currentTarget;
1292         },
1293
1294         handleDragLeave: function(ev) {
1295                 ev.currentTarget.classList.remove('drag-over-above');
1296                 ev.currentTarget.classList.remove('drag-over-below');
1297         },
1298
1299         handleDragEnd: function(ev) {
1300                 var n = ev.target;
1301
1302                 n.style.opacity = '';
1303                 n.classList.add('flash');
1304                 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1305                         .forEach(function(tr) {
1306                                 tr.classList.remove('drag-over-above');
1307                                 tr.classList.remove('drag-over-below');
1308                         });
1309         },
1310
1311         handleDrop: function(ev) {
1312                 var s = scope.dragState;
1313
1314                 if (s.node && s.targetNode) {
1315                         var config_name = this.uciconfig || this.map.config,
1316                             ref_node = s.targetNode,
1317                             after = false;
1318
1319                     if (ref_node.classList.contains('drag-over-below')) {
1320                         ref_node = ref_node.nextElementSibling;
1321                         after = true;
1322                     }
1323
1324                     var sid1 = s.node.getAttribute('data-sid'),
1325                         sid2 = s.targetNode.getAttribute('data-sid');
1326
1327                     s.node.parentNode.insertBefore(s.node, ref_node);
1328                     this.map.data.move(config_name, sid1, sid2, after);
1329                 }
1330
1331                 scope.dragState = null;
1332                 ev.target.style.opacity = '';
1333                 ev.stopPropagation();
1334                 ev.preventDefault();
1335                 return false;
1336         },
1337
1338         handleModalCancel: function(modalMap, ev) {
1339                 return Promise.resolve(L.ui.hideModal());
1340         },
1341
1342         handleModalSave: function(modalMap, ev) {
1343                 return modalMap.save()
1344                         .then(L.bind(this.map.load, this.map))
1345                         .then(L.bind(this.map.reset, this.map))
1346                         .then(L.ui.hideModal)
1347                         .catch(function() {});
1348         },
1349
1350         addModalOptions: function(modalSection, section_id, ev) {
1351
1352         },
1353
1354         renderMoreOptionsModal: function(section_id, ev) {
1355                 var parent = this.map,
1356                     title = parent.title,
1357                     name = null,
1358                     m = new CBIMap(this.map.config, null, null),
1359                     s = m.section(CBINamedSection, section_id, this.sectiontype);
1360
1361                 m.parent = parent;
1362
1363                 s.tabs = this.tabs;
1364                 s.tab_names = this.tab_names;
1365
1366                 if ((name = this.titleFn('modaltitle', section_id)) != null)
1367                         title = name;
1368                 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1369                         title = '%s - %s'.format(parent.title, name);
1370                 else if (!this.anonymous)
1371                         title = '%s - %s'.format(parent.title, section_id);
1372
1373                 for (var i = 0; i < this.children.length; i++) {
1374                         var o1 = this.children[i];
1375
1376                         if (o1.modalonly === false)
1377                                 continue;
1378
1379                         var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1380
1381                         for (var k in o1) {
1382                                 if (!o1.hasOwnProperty(k))
1383                                         continue;
1384
1385                                 switch (k) {
1386                                 case 'map':
1387                                 case 'section':
1388                                 case 'option':
1389                                 case 'title':
1390                                 case 'description':
1391                                         continue;
1392
1393                                 default:
1394                                         o2[k] = o1[k];
1395                                 }
1396                         }
1397                 }
1398
1399                 return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1400                         L.ui.showModal(title, [
1401                                 nodes,
1402                                 E('div', { 'class': 'right' }, [
1403                                         E('button', {
1404                                                 'class': 'btn',
1405                                                 'click': L.ui.createHandlerFn(this, 'handleModalCancel', m)
1406                                         }, [ _('Dismiss') ]), ' ',
1407                                         E('button', {
1408                                                 'class': 'cbi-button cbi-button-positive important',
1409                                                 'click': L.ui.createHandlerFn(this, 'handleModalSave', m)
1410                                         }, [ _('Save') ])
1411                                 ])
1412                         ], 'cbi-modal');
1413                 }, this)).catch(L.error);
1414         }
1415 });
1416
1417 var CBIGridSection = CBITableSection.extend({
1418         tab: function(name, title, description) {
1419                 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1420         },
1421
1422         handleAdd: function(ev, name) {
1423                 var config_name = this.uciconfig || this.map.config,
1424                     section_id = this.map.data.add(config_name, this.sectiontype, name);
1425
1426                 this.addedSection = section_id;
1427                 return this.renderMoreOptionsModal(section_id);
1428         },
1429
1430         handleModalSave: function(/* ... */) {
1431                 return this.super('handleModalSave', arguments)
1432                         .then(L.bind(function() { this.addedSection = null }, this));
1433         },
1434
1435         handleModalCancel: function(/* ... */) {
1436                 var config_name = this.uciconfig || this.map.config;
1437
1438                 if (this.addedSection != null) {
1439                         this.map.data.remove(config_name, this.addedSection);
1440                         this.addedSection = null;
1441                 }
1442
1443                 return this.super('handleModalCancel', arguments);
1444         },
1445
1446         renderUCISection: function(section_id) {
1447                 return this.renderOptions(null, section_id);
1448         },
1449
1450         renderChildren: function(tab_name, section_id, in_table) {
1451                 var tasks = [], index = 0;
1452
1453                 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1454                         if (opt.disable || opt.modalonly)
1455                                 continue;
1456
1457                         if (opt.editable)
1458                                 tasks.push(opt.render(index++, section_id, in_table));
1459                         else
1460                                 tasks.push(this.renderTextValue(section_id, opt));
1461                 }
1462
1463                 return Promise.all(tasks);
1464         },
1465
1466         renderTextValue: function(section_id, opt) {
1467                 var title = this.stripTags(opt.title).trim(),
1468                     descr = this.stripTags(opt.description).trim(),
1469                     value = opt.textvalue(section_id);
1470
1471                 return E('div', {
1472                         'class': 'td cbi-value-field',
1473                         'data-title': (title != '') ? title : null,
1474                         'data-description': (descr != '') ? descr : null,
1475                         'data-name': opt.option,
1476                         'data-widget': opt.typename || opt.__name__
1477                 }, (value != null) ? value : E('em', _('none')));
1478         },
1479
1480         renderHeaderRows: function(section_id) {
1481                 return this.super('renderHeaderRows', [ NaN, true ]);
1482         },
1483
1484         renderRowActions: function(section_id) {
1485                 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1486         },
1487
1488         parse: function() {
1489                 var section_ids = this.cfgsections(),
1490                     tasks = [];
1491
1492                 if (Array.isArray(this.children)) {
1493                         for (var i = 0; i < section_ids.length; i++) {
1494                                 for (var j = 0; j < this.children.length; j++) {
1495                                         if (!this.children[j].editable || this.children[j].modalonly)
1496                                                 continue;
1497
1498                                         tasks.push(this.children[j].parse(section_ids[i]));
1499                                 }
1500                         }
1501                 }
1502
1503                 return Promise.all(tasks);
1504         }
1505 });
1506
1507 var CBINamedSection = CBIAbstractSection.extend({
1508         __name__: 'CBI.NamedSection',
1509         __init__: function(map, section_id /*, ... */) {
1510                 this.super('__init__', this.varargs(arguments, 2, map));
1511
1512                 this.section = section_id;
1513         },
1514
1515         cfgsections: function() {
1516                 return [ this.section ];
1517         },
1518
1519         handleAdd: function(ev) {
1520                 var section_id = this.section,
1521                     config_name = this.uciconfig || this.map.config;
1522
1523                 this.map.data.add(config_name, this.sectiontype, section_id);
1524                 return this.map.save(null, true);
1525         },
1526
1527         handleRemove: function(ev) {
1528                 var section_id = this.section,
1529                     config_name = this.uciconfig || this.map.config;
1530
1531                 this.map.data.remove(config_name, section_id);
1532                 return this.map.save(null, true);
1533         },
1534
1535         renderContents: function(data) {
1536                 var ucidata = data[0], nodes = data[1],
1537                     section_id = this.section,
1538                     config_name = this.uciconfig || this.map.config,
1539                     sectionEl = E('div', {
1540                                 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1541                                 'class': 'cbi-section',
1542                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
1543                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
1544                         });
1545
1546                 if (typeof(this.title) === 'string' && this.title !== '')
1547                         sectionEl.appendChild(E('legend', {}, this.title));
1548
1549                 if (typeof(this.description) === 'string' && this.description !== '')
1550                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1551
1552                 if (ucidata) {
1553                         if (this.addremove) {
1554                                 sectionEl.appendChild(
1555                                         E('div', { 'class': 'cbi-section-remove right' },
1556                                                 E('button', {
1557                                                         'class': 'cbi-button',
1558                                                         'click': L.ui.createHandlerFn(this, 'handleRemove')
1559                                                 }, [ _('Delete') ])));
1560                         }
1561
1562                         sectionEl.appendChild(E('div', {
1563                                 'id': 'cbi-%s-%s'.format(config_name, section_id),
1564                                 'class': this.tabs
1565                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1566                                 'data-section-id': section_id
1567                         }, nodes));
1568                 }
1569                 else if (this.addremove) {
1570                         sectionEl.appendChild(
1571                                 E('button', {
1572                                         'class': 'cbi-button cbi-button-add',
1573                                         'click': L.ui.createHandlerFn(this, 'handleAdd')
1574                                 }, [ _('Add') ]));
1575                 }
1576
1577                 L.dom.bindClassInstance(sectionEl, this);
1578
1579                 return sectionEl;
1580         },
1581
1582         render: function() {
1583                 var config_name = this.uciconfig || this.map.config,
1584                     section_id = this.section;
1585
1586                 return Promise.all([
1587                         this.map.data.get(config_name, section_id),
1588                         this.renderUCISection(section_id)
1589                 ]).then(this.renderContents.bind(this));
1590         }
1591 });
1592
1593 var CBIValue = CBIAbstractValue.extend({
1594         __name__: 'CBI.Value',
1595
1596         value: function(key, val) {
1597                 this.keylist = this.keylist || [];
1598                 this.keylist.push(String(key));
1599
1600                 this.vallist = this.vallist || [];
1601                 this.vallist.push(L.dom.elem(val) ? val : String(val != null ? val : key));
1602         },
1603
1604         render: function(option_index, section_id, in_table) {
1605                 return Promise.resolve(this.cfgvalue(section_id))
1606                         .then(this.renderWidget.bind(this, section_id, option_index))
1607                         .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1608         },
1609
1610         renderFrame: function(section_id, in_table, option_index, nodes) {
1611                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1612                     depend_list = this.transformDepList(section_id),
1613                     optionEl;
1614
1615                 if (in_table) {
1616                         var title = this.stripTags(this.title).trim();
1617                         optionEl = E('div', {
1618                                 'class': 'td cbi-value-field',
1619                                 'data-title': (title != '') ? title : null,
1620                                 'data-description': this.stripTags(this.description).trim(),
1621                                 'data-name': this.option,
1622                                 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1623                         }, E('div', {
1624                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1625                                 'data-index': option_index,
1626                                 'data-depends': depend_list,
1627                                 'data-field': this.cbid(section_id)
1628                         }));
1629                 }
1630                 else {
1631                         optionEl = E('div', {
1632                                 'class': 'cbi-value',
1633                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1634                                 'data-index': option_index,
1635                                 'data-depends': depend_list,
1636                                 'data-field': this.cbid(section_id),
1637                                 'data-name': this.option,
1638                                 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1639                         });
1640
1641                         if (this.last_child)
1642                                 optionEl.classList.add('cbi-value-last');
1643
1644                         if (typeof(this.title) === 'string' && this.title !== '') {
1645                                 optionEl.appendChild(E('label', {
1646                                         'class': 'cbi-value-title',
1647                                         'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
1648                                         'click': function(ev) {
1649                                                 var node = ev.currentTarget,
1650                                                     elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
1651
1652                                                 if (elem) {
1653                                                         elem.click();
1654                                                         elem.focus();
1655                                                 }
1656                                         }
1657                                 },
1658                                 this.titleref ? E('a', {
1659                                         'class': 'cbi-title-ref',
1660                                         'href': this.titleref,
1661                                         'title': this.titledesc || _('Go to relevant configuration page')
1662                                 }, this.title) : this.title));
1663
1664                                 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1665                         }
1666                 }
1667
1668                 if (nodes)
1669                         (optionEl.lastChild || optionEl).appendChild(nodes);
1670
1671                 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1672                         L.dom.append(optionEl.lastChild || optionEl,
1673                                 E('div', { 'class': 'cbi-value-description' }, this.description));
1674
1675                 if (depend_list && depend_list.length)
1676                         optionEl.classList.add('hidden');
1677
1678                 optionEl.addEventListener('widget-change',
1679                         L.bind(this.map.checkDepends, this.map));
1680
1681                 L.dom.bindClassInstance(optionEl, this);
1682
1683                 return optionEl;
1684         },
1685
1686         renderWidget: function(section_id, option_index, cfgvalue) {
1687                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1688                     choices = this.transformChoices(),
1689                     widget;
1690
1691                 if (choices) {
1692                         var placeholder = (this.optional || this.rmempty)
1693                                 ? E('em', _('unspecified')) : _('-- Please choose --');
1694
1695                         widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1696                                 id: this.cbid(section_id),
1697                                 sort: this.keylist,
1698                                 optional: this.optional || this.rmempty,
1699                                 datatype: this.datatype,
1700                                 select_placeholder: this.placeholder || placeholder,
1701                                 validate: L.bind(this.validate, this, section_id)
1702                         });
1703                 }
1704                 else {
1705                         widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1706                                 id: this.cbid(section_id),
1707                                 password: this.password,
1708                                 optional: this.optional || this.rmempty,
1709                                 datatype: this.datatype,
1710                                 placeholder: this.placeholder,
1711                                 validate: L.bind(this.validate, this, section_id)
1712                         });
1713                 }
1714
1715                 return widget.render();
1716         }
1717 });
1718
1719 var CBIDynamicList = CBIValue.extend({
1720         __name__: 'CBI.DynamicList',
1721
1722         renderWidget: function(section_id, option_index, cfgvalue) {
1723                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1724                     choices = this.transformChoices(),
1725                     items = L.toArray(value);
1726
1727                 var widget = new ui.DynamicList(items, choices, {
1728                         id: this.cbid(section_id),
1729                         sort: this.keylist,
1730                         optional: this.optional || this.rmempty,
1731                         datatype: this.datatype,
1732                         placeholder: this.placeholder,
1733                         validate: L.bind(this.validate, this, section_id)
1734                 });
1735
1736                 return widget.render();
1737         },
1738 });
1739
1740 var CBIListValue = CBIValue.extend({
1741         __name__: 'CBI.ListValue',
1742
1743         __init__: function() {
1744                 this.super('__init__', arguments);
1745                 this.widget = 'select';
1746                 this.deplist = [];
1747         },
1748
1749         renderWidget: function(section_id, option_index, cfgvalue) {
1750                 var choices = this.transformChoices();
1751                 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1752                         id: this.cbid(section_id),
1753                         size: this.size,
1754                         sort: this.keylist,
1755                         optional: this.optional,
1756                         placeholder: this.placeholder,
1757                         validate: L.bind(this.validate, this, section_id)
1758                 });
1759
1760                 return widget.render();
1761         },
1762 });
1763
1764 var CBIFlagValue = CBIValue.extend({
1765         __name__: 'CBI.FlagValue',
1766
1767         __init__: function() {
1768                 this.super('__init__', arguments);
1769
1770                 this.enabled = '1';
1771                 this.disabled = '0';
1772                 this.default = this.disabled;
1773         },
1774
1775         renderWidget: function(section_id, option_index, cfgvalue) {
1776                 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1777                         id: this.cbid(section_id),
1778                         value_enabled: this.enabled,
1779                         value_disabled: this.disabled,
1780                         validate: L.bind(this.validate, this, section_id)
1781                 });
1782
1783                 return widget.render();
1784         },
1785
1786         formvalue: function(section_id) {
1787                 var elem = this.getUIElement(section_id),
1788                     checked = elem ? elem.isChecked() : false;
1789                 return checked ? this.enabled : this.disabled;
1790         },
1791
1792         textvalue: function(section_id) {
1793                 var cval = this.cfgvalue(section_id);
1794
1795                 if (cval == null)
1796                         cval = this.default;
1797
1798                 return (cval == this.enabled) ? _('Yes') : _('No');
1799         },
1800
1801         parse: function(section_id) {
1802                 if (this.isActive(section_id)) {
1803                         var fval = this.formvalue(section_id);
1804
1805                         if (!this.isValid(section_id))
1806                                 return Promise.reject();
1807
1808                         if (fval == this.default && (this.optional || this.rmempty))
1809                                 return Promise.resolve(this.remove(section_id));
1810                         else
1811                                 return Promise.resolve(this.write(section_id, fval));
1812                 }
1813                 else {
1814                         return Promise.resolve(this.remove(section_id));
1815                 }
1816         },
1817 });
1818
1819 var CBIMultiValue = CBIDynamicList.extend({
1820         __name__: 'CBI.MultiValue',
1821
1822         __init__: function() {
1823                 this.super('__init__', arguments);
1824                 this.placeholder = _('-- Please choose --');
1825         },
1826
1827         renderWidget: function(section_id, option_index, cfgvalue) {
1828                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1829                     choices = this.transformChoices();
1830
1831                 var widget = new ui.Dropdown(L.toArray(value), choices, {
1832                         id: this.cbid(section_id),
1833                         sort: this.keylist,
1834                         multiple: true,
1835                         optional: this.optional || this.rmempty,
1836                         select_placeholder: this.placeholder,
1837                         display_items: this.display_size || this.size || 3,
1838                         dropdown_items: this.dropdown_size || this.size || -1,
1839                         validate: L.bind(this.validate, this, section_id)
1840                 });
1841
1842                 return widget.render();
1843         },
1844 });
1845
1846 var CBITextValue = CBIValue.extend({
1847         __name__: 'CBI.TextValue',
1848
1849         value: null,
1850
1851         renderWidget: function(section_id, option_index, cfgvalue) {
1852                 var value = (cfgvalue != null) ? cfgvalue : this.default;
1853
1854                 var widget = new ui.Textarea(value, {
1855                         id: this.cbid(section_id),
1856                         optional: this.optional || this.rmempty,
1857                         placeholder: this.placeholder,
1858                         monospace: this.monospace,
1859                         cols: this.cols,
1860                         rows: this.rows,
1861                         wrap: this.wrap,
1862                         validate: L.bind(this.validate, this, section_id)
1863                 });
1864
1865                 return widget.render();
1866         }
1867 });
1868
1869 var CBIDummyValue = CBIValue.extend({
1870         __name__: 'CBI.DummyValue',
1871
1872         renderWidget: function(section_id, option_index, cfgvalue) {
1873                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1874                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1875                     outputEl = E('div');
1876
1877                 if (this.href)
1878                         outputEl.appendChild(E('a', { 'href': this.href }));
1879
1880                 L.dom.append(outputEl.lastChild || outputEl,
1881                         this.rawhtml ? value : [ value ]);
1882
1883                 return E([
1884                         outputEl,
1885                         hiddenEl.render()
1886                 ]);
1887         },
1888
1889         remove: function() {},
1890         write: function() {}
1891 });
1892
1893 var CBIButtonValue = CBIValue.extend({
1894         __name__: 'CBI.ButtonValue',
1895
1896         renderWidget: function(section_id, option_index, cfgvalue) {
1897                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1898                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1899                     outputEl = E('div'),
1900                     btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1901
1902                 if (value !== false)
1903                         L.dom.content(outputEl, [
1904                                 E('button', {
1905                                         'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1906                                         'click': L.ui.createHandlerFn(this, function(section_id, ev) {
1907                                                 if (this.onclick)
1908                                                         return this.onclick(ev, section_id);
1909
1910                                                 ev.currentTarget.parentNode.nextElementSibling.value = value;
1911                                                 return this.map.save();
1912                                         }, section_id)
1913                                 }, [ btn_title ])
1914                         ]);
1915                 else
1916                         L.dom.content(outputEl, ' - ');
1917
1918                 return E([
1919                         outputEl,
1920                         hiddenEl.render()
1921                 ]);
1922         }
1923 });
1924
1925 var CBIHiddenValue = CBIValue.extend({
1926         __name__: 'CBI.HiddenValue',
1927
1928         renderWidget: function(section_id, option_index, cfgvalue) {
1929                 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1930                         id: this.cbid(section_id)
1931                 });
1932
1933                 return widget.render();
1934         }
1935 });
1936
1937 var CBIFileUpload = CBIValue.extend({
1938         __name__: 'CBI.FileSelect',
1939
1940         __init__: function(/* ... */) {
1941                 this.super('__init__', arguments);
1942
1943                 this.show_hidden = false;
1944                 this.enable_upload = true;
1945                 this.enable_remove = true;
1946                 this.root_directory = '/etc/luci-uploads';
1947         },
1948
1949         renderWidget: function(section_id, option_index, cfgvalue) {
1950                 var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
1951                         id: this.cbid(section_id),
1952                         name: this.cbid(section_id),
1953                         show_hidden: this.show_hidden,
1954                         enable_upload: this.enable_upload,
1955                         enable_remove: this.enable_remove,
1956                         root_directory: this.root_directory
1957                 });
1958
1959                 return browserEl.render();
1960         }
1961 });
1962
1963 var CBISectionValue = CBIValue.extend({
1964         __name__: 'CBI.ContainerValue',
1965         __init__: function(map, section, option, cbiClass /*, ... */) {
1966                 this.super('__init__', [map, section, option]);
1967
1968                 if (!CBIAbstractSection.isSubclass(cbiClass))
1969                         throw 'Sub section must be a descendent of CBIAbstractSection';
1970
1971                 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1972                 this.subsection.parentoption = this;
1973         },
1974
1975         load: function(section_id) {
1976                 return this.subsection.load();
1977         },
1978
1979         parse: function(section_id) {
1980                 return this.subsection.parse();
1981         },
1982
1983         renderWidget: function(section_id, option_index, cfgvalue) {
1984                 return this.subsection.render();
1985         },
1986
1987         checkDepends: function(section_id) {
1988                 this.subsection.checkDepends();
1989                 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1990         },
1991
1992         write: function() {},
1993         remove: function() {},
1994         cfgvalue: function() { return null },
1995         formvalue: function() { return null }
1996 });
1997
1998 return L.Class.extend({
1999         Map: CBIMap,
2000         JSONMap: CBIJSONMap,
2001         AbstractSection: CBIAbstractSection,
2002         AbstractValue: CBIAbstractValue,
2003
2004         TypedSection: CBITypedSection,
2005         TableSection: CBITableSection,
2006         GridSection: CBIGridSection,
2007         NamedSection: CBINamedSection,
2008
2009         Value: CBIValue,
2010         DynamicList: CBIDynamicList,
2011         ListValue: CBIListValue,
2012         Flag: CBIFlagValue,
2013         MultiValue: CBIMultiValue,
2014         TextValue: CBITextValue,
2015         DummyValue: CBIDummyValue,
2016         Button: CBIButtonValue,
2017         HiddenValue: CBIHiddenValue,
2018         FileUpload: CBIFileUpload,
2019         SectionValue: CBISectionValue
2020 });