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